I have a problem to send data, in this case on the delivery address, the products purchased, the date of purchase, the user who made this purchase.
I have a problem with when to send this data and how?
I use the stripe api to realize the payment, everything works the user is redirected on the stripe link with his session, and pay according to the items in his cart. But my checkout data is lost.
The ideal would be that when stripe confirms the purchase and thus redirects on the url of success, which is the case, and well the quoted data are transferred.
So I have this code:
HTML CHECKOUT
<div class="form">
<p class="text">Adresse de livraison</p>
<input type="text" id="address" placeholder="adresse">
<div class="two-input-container">
<input type="text" id="city" placeholder="ville">
<input type="number" id="state" placeholder="code postale">
</div>
<input type="text" id="landmark" placeholder="informations complémentaire">
</div>
CHECKOUT JS
window.onload = () => {
if(!sessionStorage.user){
location.replace('/login')
}
if(location.search.includes('payment=done')){
let items = [];
localStorage.setItem('cart', JSON.stringify(items));
showFormError("order is placed");
}
if(location.search.includes('payment_fail=true')){
showFormError("Une erreur est survenue, merci de réessayer");
}
}
// select place order button
const placeOrderBtn = document.querySelector('.place-order-btn');
const getAddress = () => {
// form validation
let address = document.querySelector('#address').value;
let city = document.querySelector('#city').value;
let state = document.querySelector('#state').value;
let landmark = document.querySelector('#landmark').value;
if(!address.length || !city.length || !state.length){
return showFormError("Remplisser tous les champs");
} else{
return { address, city, state, landmark }
}
}
placeOrderBtn.addEventListener('click', () => {
let cart = JSON.parse(localStorage.getItem('cart'));
if(cart == null || !cart.length){
return showFormError("Vous commander aucun article");
}
else{
let address = getAddress();
if(address.address.length){
// send data to backend
fetch('/stipe-checkout', {
method: 'post',
headers: new Headers({'Content-Type': 'application/json'}),
body: JSON.stringify({
items: JSON.parse(localStorage.getItem('cart')),
address: address,
email: JSON.parse(sessionStorage.user).email
})
})
.then(res => res.json())
.then(url => {
location.href = url;
})
.catch(err => console.log(err))
}
}
})
SERVER index.JS
// stripe payment
let stripeGateway = stripe(process.env.stripe_key);
let DOMAIN = process.env.DOMAIN;
app.post('/stipe-checkout', async (req, res) => {
const session = await stripeGateway.checkout.sessions.create({
payment_method_types: ["card"],
mode: "payment",
success_url: `${DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}&order=${JSON.stringify(req.body)}`,
cancel_url: `${DOMAIN}/checkout?payment_fail=true`,
line_items: req.body.items.map(item => {
return {
price_data: {
currency: "eur",
product_data: {
name: item.name,
description: item.shortDes,
images: item.images
},
unit_amount: item.price * 100
},
quantity: item.item
}
})
})
res.json(session.url)
})
app.get('/success', async (req, res) => {
let { order, session_id } = req.query;
try{
const session = await stripeGateway.checkout.sessions.retrieve(session_id);
const customer = await stripeGateway.customers.retrieve(session.customer);
let date = new Date();
let orders_collection = collection(db, "orders");
let docName = `${customer.email}-order-${date.getTime()}`;
setDoc(doc(orders_collection, docName), JSON.parse(order))
.then(data => {
res.redirect('/checkout?payment=done')
})
} catch{
res.redirect("/404");
}
})
I've tried to send it before but in fact if someone click to the button to redirect to stripe confirm page, his delivered data, will be stored even if he don't confirm his buy.
So when your customers trigger the redirect to Stripe Checkout, the address data you want to retain is present in the req variable in your app.post('/stripe-checkout'?
In that case you can include this data when creating the Checkout Session in the payment_intent_data.shipping parameter. Then this data will be stored on the shipping property of the Payment Intent that the Checkout creates. You can get this data when you retrieve the Checkout Session (either server-side or client-side) by passing payment_intent in the Expand parameter and looking at
the payment_intent.shipping.address property on the Checkout session object.
Alternatively, you could have the Checkout Session itself collect shipping address information for you. This would store the address in the shipping_details.address property.
To ensure your integration is able to get the customer's address information I recommend you configure your integration to listen to the checkout.session.completed webhook. That way, even if the customer fails to redirect back to your site, your back-end code will still be able to get the address information.
Related
As the title suggests, I am trying to implement Stripe into my flutter app using the stripe extension for Firebase and using Javascript Firebase Cloud Functions for the server side. I believe the issue is on the server side when I try to create a customer and create a payment intent.
The server side code is here:
const functions = require("firebase-functions");
const stripe = require("stripe")("my test secret key"); // this works fine for the other stripe functions I am calling
exports.stripePaymentIntentRequest = functions.https.onRequest(
async (req, res) => {
const {email, amount} = req.body;
try {
let customerId;
// Gets the customer who's email id matches the one sent by the client
const customerList = await stripe.customers.list({
email: email,
limit: 1,
});
// Checks the if the customer exists, if not creates a new customer
if (customerList.data.length !== 0) {
customerId = customerList.data[0].id;
} else {
const customer = await stripe.customers.create({
email: email,
});
customerId = customer.data.id;
}
// Creates a temporary secret key linked with the customer
const ephemeralKey = await stripe.ephemeralKeys.create(
{customer: customerId},
{apiVersion: "2022-11-15"},
);
// Creates a new payment intent with amount passed in from the client
const paymentIntent = await stripe.paymentIntents.create({
amount: parseInt(amount),
currency: "gbp",
customer: customerId,
});
res.status(200).send({
paymentIntent: paymentIntent.client_secret,
ephemeralKey: ephemeralKey.secret,
customer: customerId,
success: true,
});
} catch (error) {
res.status(404).send({success: false, error: error.message});
}
},
);
Then my client-side code is:
try {
// 1. create payment intent on the server
final response = await http.post(
Uri.parse(
'https://us-central1-clublink-1.cloudfunctions.net/stripePaymentIntentRequest'),
headers: {"Content-Type": "application/json"},
body: json.encode({
'email': email,
'amount': amount.toString(),
}),
);
final jsonResponse = json.decode(response.body);
if (jsonResponse['error'] != null) {
throw Exception(jsonResponse['error']);
}
log(jsonResponse.toString());
//2. initialize the payment sheet
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: jsonResponse['paymentIntent'],
merchantDisplayName: 'Clublink UK',
customerId: jsonResponse['customer'],
customerEphemeralKeySecret: jsonResponse['ephemeralKey'],
style: ThemeMode.dark,
),
);
await Stripe.instance.presentPaymentSheet();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Payment completed!')),
);
} catch (e) {
if (e is StripeException) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error from Stripe: ${e.error.localizedMessage}'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
}
I basically copied the flutter_stripe documentation to create the payment sheet with the necessary changes. Any help would be greatly appreciated!
Ok so I found what worked! I was being given a 403 status error with reason "forbidden". This meant I had to go to the google cloud console and update the permissions in the cloud functions tab.
After a long discussion with ChatGPT, I managed to write code that redirects the user to the Stripe payment page and then captures an event when the transaction is successfully completed.
The problem is that my fetch request has already received a response from the /checkout endpoint and is not waiting for a response from /webhook. And I would like my API to return a properly generated response after successfully finalizing the transaction. What am I doing wrong?
First, I send a request to the /checkout endpoint, which takes care of generating the payment link and sending it back:
fetch('http://localhost:3001/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
id: 0,
},
],
}),
})
.then((res) => {
if (res.ok) return res.json();
return res.json().then((e) => console.error(e));
})
.then(({url}) => {
console.log(url);
window.location = url;
})
.catch((e) => {
console.log(e);
});
This code when I press the button redirects me to the Stripe payment page.
Endpoint /checkout:
app.post('/checkout', async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: req.body.items.map(({id}) => {
const storeItem = storeItems.get(id);
return {
price_data: {
currency: 'pln',
product_data: {
name: storeItem.name,
},
unit_amount: storeItem.priceInCents,
},
quantity: 1,
};
}),
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/success.html`,
cancel_url: `${process.env.CLIENT_URL}/cancel.html`,
});
console.log(session.url);
res.json({url: session.url});
} catch (e) {
// If there is an error send it to the client
console.log(e.message);
res.status(500).json({error: e.message});
}
});
I connected StripeCLI to my server using stripe listen --forward-to localhost:3001/webhook. Now I can capture the successful transaction event using the /webhook endpoint, but I have no way to return The transaction was successful to the client:
app.post('/webhook', (req, res) => {
const event = req.body;
if (event.type === 'checkout.session.completed') {
res.send('The transaction was successful');
}
});
After the suceesful payment the customer should be redirected back to your website. Where you can create success page.
success_url: `${process.env.CLIENT_URL}/success.html`,
If you want to get some data back from the Strapi after the paymant is successful page you can add this
success_url: `${process.env.CLIENT_URL}/success.html?&session_id={CHECKOUT_SESSION_ID}`
At the succes page you just deconstruct the data. And do whatever you want with them :)
If you deconstruct the object for example like this: (Next.js)
const stripe = require("stripe")(`${process.env.STRIPE_SECRET_KEY}`);
export async function getServerSideProps(params) {
const order = await stripe.checkout.sessions.retrieve(
params.query.session_id,
{
expand: ["line_items"],
},
);
const shippingRate = await stripe.shippingRates.retrieve(
"shr_1MJv",
);
return { props: { order, shippingRate } };
}
export default function Success({ order, shippingRate }) {
const route = useRouter();
Yo can log out the whole object to see whats inside
console.log(order);
If the payment was sucessfull you should get in prop variable: payment_status: "paid"
Stripe will automatically redirect the client to the success_url that you specified when you created a Stripe session.
You can use the webhook for saving the order in the database for example, but not to redirect the client.
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 two ways to register a user in Firebase: through email and through Google Sign In.
I perform the user registration by email as follows:
signUp() {
const auth = getAuth();
const db = getFirestore();
createUserWithEmailAndPassword(
auth,
this.createEmail,
this.createPassword
).then(
(userCredential) => {
const user = userCredential.user;
this.$router.push("/");
addDoc(collection(db, "users"), {
email: this.createEmail,
name: this.createName,
});
},
);
},
In other words, in addition to saving the user in Firebase Authentication, I also send their name and email to Firestore. And this is my first question:
Is it the most effective way to save the username and future data that will still be added to it?
Finally, login by Google is done as follows:
googleSignIn() {
const auth = getAuth();
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
this.$router.push("/");
addDoc(collection(db, "users"), {
email: result.user.email,
name: result.user.displayName,
});
})
},
Here a problem arises because if a user logs in more than once in Firebase Authentication everything is ok, but in Firebase Firestore a user is created for each new login with Google.
How do I handle this issue of storing users in Firestore, especially users coming from Google Login?
First, I'd move the router.push() statement below addDoc() so I can confirm that the document has been added and then user is redirected to other pages. In case of Google SignIn, you can check if the user is new by accessing the isNewUser property by fetching additional information. If true, then add document to Firestore else redirect to dashboard:
signInWithPopup(auth, provider)
.then(async (result) => {
// Check if user is new
const {isNewUser} = getAdditionalUserInfo(result)
if (isNewUser) {
await addDoc(collection(db, "users"), {
email: result.user.email,
name: result.user.displayName,
});
}
this.$router.push("/");
})
It might be a good idea to set the document ID as user's Firebase Auth UID instead of using addDoc() which generated another random ID so it's easier to write security rules. Try refactoring the code to this:
signInWithPopup(auth, provider)
.then(async (result) => {
// Check if user is new
const {isNewUser} = getAdditionalUserInfo(result)
const userId = result.user.uid
if (isNewUser) {
await setDoc(doc(db, "users", userId), {
email: result.user.email,
name: result.user.displayName,
});
}
this.$router.push("/");
})
I want users to pay a fee before a POST request from a front end form is processed. I have a Stripe webhook that works fine on the backend, but I'm not sure how to delay the front end posting of the form until after the payment confirmation is received.
In the code below, right now, createTour and createTourPay run at the same time. I would like for createTourPay to execute first, and the createTour only triggers after Stripe posts to my application from the webhook. How can I achieve this?
Controller File (webhook):
exports.webhookCheckout = (req, res, next) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (
event.type === 'checkout.session.completed' &&
event.line_items.name === 'New Job Purchase'
) {
res.status(200).json({ recieved: true });
// Somehow, I want this to trigger the execution of the POST request in my front end JS file.
} else {
if (event.type === 'checkout.session.completed')
createBookingCheckout(event.data.object);
res.status(200).json({ recieved: true });
}
};
Front end JS file:
export const createTourPay = async myForm => {
try {
// 1) Get the checkout session from API response
const session = await axios(`/api/v1/tours/tour-pay`);
const complete = 1;
// console.log(session);
// 2) Create checkout form + charge the credit card
await stripe.redirectToCheckout({
sessionId: session.data.session.id
});
} catch (err) {
// console.log(err);
showAlert('error', err);
}
};
export const createTour = async myForm => {
try {
const startLocation = {
type: 'Point',
coordinates: [-10.185942, 95.774772],
address: '123 Main Street',
description: 'Candy Land'
};
const res = await axios({
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${myForm._boundary}`
},
url: '/api/v1/tours',
data: myForm
});
if (res.data.status === 'success') {
showAlert('success', 'NEW TOUR CREATED!');
window.setTimeout(() => {
location.assign('/');
}, 1500);
}
} catch (err) {
showAlert('error', err.response.data.message);
}
};
Broadly: don't do this. Instead, you in fact should create some pending/unpaid version of the "tour" (or any other product/service) in your system, then attach the unique id (eg: tour_123) to the Checkout session when you create it, either using the client_reference_id (doc) or metadata (doc):
const session = await stripe.checkout.sessions.create({
// ... other params
client_reference_id: 'tour_123',
metadata: { tour_id: 'tour_123' },
});
Then you'd use the webhook to inspect those values, and update your own database to indicate the payment has been made and that you can fulfill the order to the customer (ship product, send codes, allow access to service etc).
If you really want to proceed with a more synchronous flow, you can use separate auth and capture to sequence your customer experience and capture the funds later after authorizing and creating your tour entity.
Edit: a note about security
You should never trust client-side logic for restricted operations like creating a "paid" tour. A motivated user could, for example, simply call your /api/v1/tours create endpoint without ever going through your payment flow. Unless you validate a payment and track that state on your server you won't be able to know which of these had actually paid you.