Stripe Checkout: moving from Node to Firebase Functions - node.js

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 😁

Related

Stripe webhook host with node.js deployment

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.

React Stripe Heroku - checkout payment not working anymore after deployment

I'm trying to implement stripe to my small e-commerce website developed in reactjs. To do so I've used the checkout component.
It works perfectly when i'm on localhost, but after i deploy the backend with heroku I receive this error message :
Apparently i didn't provide the API key but I feel like I did... I don't understand ...
Anyone ?
On the front end :
// Linking with the back end
const makePayment = (token) => {
const body = {
token: token,
product: product,
};
const headers = {
"Content-Type": "application/json",
};
return fetch(`https://serotonineneshop.herokuapp.com/payment`, {
method: "POST",
headers: headers,
body: JSON.stringify(body),
})
.then((response) => {
console.log("RESPONSE : ", response);
const { status } = response;
console.log("STATUS : ", status);
})
.catch((err) => console.log(err));
};
on the back-end :
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY)
app.post("/payment", (req, res) => {
const {
product,
token
} = req.body;
const idempotencyKey = uuidv4()
return stripe.customers.create({
email: token.email,
source: token.id
}).then(customer => {
stripe.charges.create({
amount: product.price * 100,
currency: "eur",
customer: customer.id,
receipt_email: token.email,
description: `purchase of ${product.name}`,
shipping: {
name: token.card.name,
phone: token.card.phone,
address: {
country: token.card.address_country,
postAddress: token.card.address,
zipCode: token.card.zipCode
}
}
}, {
idempotencyKey
})
.then(result = res.status(202).json(result))
.catch(err => console.log(err))
})
});

Stripe webhook using CLI fails to POST. Returns "Client.Timeout exceeded while awaiting headers"

I'm trying to implement basic stripe checkout webhook with CLI as described here:
https://stripe.com/docs/payments/checkout/fulfill-orders
Only difference is instead of bodyParser.raw({type: 'application/json'}), I'm using express.json({ type: 'application/json' }).
import express from 'express'
import dotenv from 'dotenv'
import connectDB from './config/db.js'
import Order from './models/orderModel.js'
import Stripe from 'stripe'
dotenv.config()
connectDB()
const app = express()
const stripe = new Stripe(`${process.env.STRIPE_SECRET_KEY}`)
app.post('/api/create-stripe-checkout-session', async (req, res) => {
const order = await Order.findById(req.body.orderId)
if (order) {
const session = await stripe.checkout.sessions.create({
success_url: 'http://localhost:3000/success?id={CHECKOUT_SESSION_ID}',
cancel_url: 'http://localhost:3000/cancel',
payment_method_types: ['card'],
mode: 'payment',
line_items: [
{
price_data: {
currency: order.currency,
unit_amount: (order.totalPrice * 100).toFixed(0),
product_data: {
name: `Order ID:${req.body.orderId}`,
},
},
quantity: 1,
},
],
})
res.json({ url: session.url })
} else {
res.status(404)
throw new Error('Order not found')
}
})
app.post('/webhook', express.json({ type: 'application/json' }), (req, res) => {
const payload = req.body
console.log('Got payload: ' + payload)
res.status(200)
})
const PORT = process.env.PORT || 5000
app.listen(
PORT,
console.log(`server running in ${process.env.NODE_ENV} mode on port ${PORT}`)
)
Response in CLI terminal:
Response in server terminal:
When I add signature verification as described in above stripe doc, POST request fails with error 400. I have tried removing unrelated middleware and testing in Linux and Windows but it gives the same result.
Stripe Node version: 8.163.0
Stripe CLI version: 1.6.4
Adding .end() in the webhook solved the problem. Stripe should update their docs. I spent two days trying to solve this.
Correct code:
app.post('/webhook', express.json({ type: 'application/json' }), (req, res) => {
const payload = req.body
console.log('Got payload: ' + payload)
res.status(200).end() //add .end() to solve the issue
})

How do I make sure customers don't skip paywall with stripe?

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.

How to use JWT with stripe to retrieve a paymentIntent?

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

Resources