Stripe succesfully creating customer without card details - javascript

Complete web/Stripe newbie here. I built an iOS app, but the method of taking payments that I wanted to use isn't allowed on iOS, so I had to set up a website for it.
The website uses HTML/CSS, with a node backend hosted on Heroku. The website is a simple site that takes the name and card details of the user, but there's currently an issue with my implementation.
In app.get(), I create a customer and a setupIntent, and then this gets filled out when the user clicks a button on the site (just an event listener in js on the client-side).
My issue is, when I create a customer it creates an empty customer every time the page is loaded. If I remove this customer, there is no extra customer being added on load, and the correct customer is created, but there is no card attached to the customer's account!
I'm sure this is a basic error on my part, as I rushed through learning web dev in order to get the app to accept payments (we got an unexpected rejection from the App Review team, basically saying our app will never be acccepted as long as it takes card details on the app).
Thanks in advance for any/all help.
Cheers!
Josh
Server-side:
const express = require("express");
const bodyParser = require("body-parser");
const ejs = require("ejs");
require('dotenv').config()
const app = express();
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({extended: true}));
app.use(express.static("public"));
const Stripe = require('stripe');
const stripe = Stripe(process.env.SECRET_KEY);
app.get('/', async (req, res) => {
var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
const customer = await stripe.customers.create({
email: fullUrl.split('=')[1] //This gets the email sent in the URL from the app
});
const intent = await stripe.setupIntents.create({
customer: customer.id,
payment_method_types: ['card'],
});
console.log(fullUrl)
console.log(fullUrl.split('=')[1])
res.render('index', { client_secret: intent.client_secret });
})
app.listen(process.env.PORT || 3000);
Client-side:
var stripe = Stripe('livePublicKeyIsHere');
// const firebase = require("firebase");
// require("firebase/firestore");
var elements = stripe.elements();
var cardElement = elements.create('card');
cardElement.mount('#card-element');
var db = firebase.firestore();
var cardholderName = document.getElementById('cardholder-name');
var setupForm = document.getElementById('setup-form');
var clientSecret = setupForm.dataset.secret;
const queryString = window.location.search;
const email = queryString.split('=')[1];
setupForm.addEventListener('submit', function(ev) {
ev.preventDefault();
stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: cardholderName.value
},
},
}
).then(function(result) {
if (result.error) {
console.log("Error!!!" + result.error.message);
window.alert("There's an error: " + result.error.message);
} else {
console.log("Success!!!");
window.alert("Account created! Download and log into the app in order to continue.");
addUserToFirestore(email)
}
});
});
function addUserToFirestore(email) {
createUserOnFirestore(email);
db.collection("Users").doc(email).collection("Settings").doc("info").set({
cardDetailsAdded: true
})
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
}
function createUserOnFirestore(email) {
db.collection("Users").doc(email).set({
exists: true
})
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
}

The reason is because you're using get instead of post. When your user clicks the button, it should fire a POST request to your server to generate a SetupIntent object which you have already done. You also should store a relation mapping between your user and the Customer that is created, so you don't always create a new Customer when user adds a new card, instead you add the new card to existing Customer object.

While using a customer is recommended, ultimately it is optional to provide a customer (API ref). You can also attach a payment method to a customer separately, as long as you do so before using it for a payment.
Note that unless attached to a customer, a payment method is one-time-use only.

Related

Axios 404 when requesting from routes folder

I have a server.js file in Express and Node.js that contains the majority of my back-end code, outside of my database config file.
The file is quite long and to improve maintainability, I would like to make it modular by splitting various components into their own files that can be imported.
I have attempted to move a database endpoint as a starting point into its own file called auth.js, located in the routes folder. The file is set up as follows:
const express = require('express');
const router = express.Router();
const db = require('../config/db');
const crypto = require("crypto"); // set up crypto middleware for hashing password and checking password hashes
const salt = crypto.randomBytes(256).toString("hex"); // create a hash salt/pepper
const iterations = 1000; // number of iterations to jumble the hash
const hashSize = 64; //set up char length of hash
const hashAlgorithm = "sha256"; // which hashing algorithm will be used
//This function returns a hash of the password, combined with the pepper and the salt.
function PasswordHash(password, salt) {
//PEPPER MUST BE MOVED TO ENV FILE WHEN READY
const pepper = "ec3fd71d14f7cab570fc94df34e60e4d8c80cdff4d1dde66c74b10ae576b88239315295adabaced63b05104bbf8d2f2f92a24aeebe8444861ba1b7efc73dafdeda6fe2bf6e7288f959832d5db953a7eab0b37ef8ad126f94616b0c1e7b3b0ce7418dff91afaa78401dacce6aee72649840e26a01d75bfac69acf8d0dd50aaddebb9397150bb0f88795cde94ea3d03fec2992fc3b5c3c7bbd8de3f8f7d693cdcca879d9aefd6e02d4457217928091a731c08f83f9927f9b19ca34ab589dd02ecc40e336e067a1f2e072ec2b3a93617ded73028ed5bc5d55f011ba5a53099312f06d649fa06fdbf49e81c8f9a81f113f95cd416d230c2cb6056189c77f889dc83d";
return crypto.pbkdf2Sync (
password,
salt + pepper,
iterations,
hashSize,
hashAlgorithm
).toString("hex");
};
// login route
router.post('/signin', (req, res) => {
const { email, password } = req.body;
// Check all emails against input
db.query(selectEmail, [email], (err, rows) => {
if (err) throw err;
// If email exists
if (rows.length > 0) {
// If password with salt and compares to database
if (PasswordHash(password, rows[0].salt) == rows[0].password) {
// Create session
req.session.firstName = rows[0].first_name;
req.session.lastName = rows[0].last_name;
req.session.username = rows[0].user_name;
req.session.ProfilePicture = rows[0].profile_picture;
console.log('Session created:', req.session); // Print session
res.send('Login successful');
}
// If password is incorrect
else {
res.send('Email or password are incorrect');
}
}
// If email does not exist
else {
res.send('Email or password are incorrect');
};
});
});
module.exports = router;
This is then used in the main server.js file by requiring the auth.js file and using it with the route '/signin':
const authRoutes = require('./routes/auth');
app.use('/signin', authRoutes);
Finally, I make a request on my React front-end application to the /signin route.
const validateRow = () => {
// Validate login
axios.post('http://localhost:8080/signin', {
email: emailInput,
password: passwordInput
})
.then((res) => {
setMessage(res.data);
//If validation passed
if (res.data === 'Login successful') {
navigate('/directory')
};
});
};
To add some context this is for a login form and validates login data inputted into the form against rows found in the database. This worked as intended until I moved the endpoint into a separate file I now receive:
AxiosError {message: 'Request failed with status code 404', name: 'AxiosError', code: 'ERR_BAD_REQUEST', config: {…}, request: XMLHttpRequest, …}
on my front end. I would like to know how to resolve this issue.
The issue is that app.use('/signin', authRoutes) makes an endpoint be "/signin/signin" not just "/signin" which you are trying to request. The simplest solution would be to change link you pass in axios.post function to "http://localhost:8080/signin/signin".
Try using relative path as suggested in this (possible duplicate):
I don't understand why my axios post request isn't working - 404 not found
You need to listen to the port 8080
const app = express()
const port = 8080
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

Is it possible to implement socket.io connection in express route?

I implement a payment service which depend on one of my express route as a callback route, so whenever a user want to make a payment, they will be redirected to this payment service link which entirely different my backend/frontend domain. After a successful payment, user will then be redirected to my express GET route (callback route), in this route is where I give users their asset and then redirect them to the frontend.
EXPECTATION
My expectation is, whenever a user make a purchase, I want a real time update on the frontend for others to see some details about the purchase without refreshing their browser.
WHAT I'VE TRIED
I had think socket.io would solve this, like adding a socket connection in the route to then push the data to the frontend. But after making lot of research, no solution seems to work for me.
HERE IS A SIMPLE CODE OF WHAT I'VE TRIED
=============================== server.js ========================
const express = require("express")
const app = express()
const http = require("http")
const cors = require("cors")
const session = require("express-session")
const runSocket = require("./runSocket")
const { Server } = require("socket.io")
app.use(cors())
app.use(express.json())
const server = http.createServer(app)
server.listen(3004, () => {
console.log("SERVER IS RUNNING")
})
const io = new Server(server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
})
const postRoute = require("./routes/postData")(io)
app.use("/post-data", postRoute)
==================================== postData Route ======================================
module.exports = function (io) {
router.post("/", async (req, res) => {
const data = req?.body?.data.message
const room = req?.body?.data?.room
io.on("connection", (socket) => {
console.log("Socket Running...")
socket.to(room).emit("the_message", data)
})
console.log("Under socket...")
return res.status(200).json({ data: req.body.data })
})
return router
}
This log: in postData route is not printing console.log("Socket Running...")
EXPECTATION
My expectation is, whenever a user make a purchase, I would like to make a real time update on the frontend for others to see some details about the purchase.
UPDATE: The Payment Gateway config looks somthing like this:
const { body } = await got.post("https://payment-provider-link", {
headers: { Authorization: "Bearer token for payment" },
json: {
email: "email#gmail.com",
amount: amount * 100,
initiate_type: "inline",
callback_url: `${BackendBaseUrl}/payment-callback`, // <<<============
},
})
Okay so you don't need the io.on("connection") in ur route. Remove that piece of code and simply change it to io.to(room).emit("the_message", data). Also make sure to have the other sockets joined the room ur trying to emit to otherwise they won't receive the data.

Req.params and req.user missing after hitting stripe checkout flow

I'm trying to integrate Stripe subscriptions billing in my app with a tiered pricing model and based on my understanding, I need to do two things:
Allow my new users to also create a stripe customer account (via the integration)
Monitor Stripe webhook 'events' to provision access while customer subscription payments are active
My userflow is as follows:
create profile in my app (saved to database) -> redirected to stripe checkout portal for billing info (saved to stripe database) -> attempt to save stripe customerId to my database so I can monitor subscription status
However, I can't figure out how to save the customerId info in my app because req.user and req.params are empty as the users are sent back to my app from the stripe billing portal
Controller function
module.exports.stripeWebhook = async (req, res) => {
let data;
const webhookSecret = stripeWebhookSecret;
if (webhookSecret) {
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.`);
return res.sendStatus(400);
}
data = event.data;
eventType = event.type;
} else {
// retrieve the event data directly from the request body.
data = req.body.data;
eventType = req.body.type;
}
switch (eventType) {
case 'payment_intent.succeeded': {
console.log('PaymentIntent was successful!');
break;
}
case 'checkout.session.completed':
// Payment is successful and the subscription is created.
// You should provision the subscription and save the customer ID to your database.
console.log(data.object.customer); <- works
const user = await User.findById(req.user.id); <- comes back empty so my next two lines of code don't work
user.stripeId.push(data.object.customer);
await user.save();
break;
default:
}
res.sendStatus(200);
};
App.js
app.use(bodyParser.raw({type: "application/json"}));
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
I included the app.js code because the bodyparser.raw has an impact on how the body comes through in my controller function.
I was counting on the req.user or req.params to find the user in my database but it's not working. How do I save the customerId to my database like the stripe comments suggest?
You should create Stripe customer account before creating checkout session for the customer.
Check if customer already have stripe_customer account (Stripe customer account). If yes, use that one. If not, create one for him and save it in database.
Set stripe_customer for the checkout session, so customer will be automatically authenticated in the Stripe checkout
You can optionally put user's _id in the metadata of the Stripe checkout session, so you access that data later in the webhook.
Note: You should create one stripe_customer account for each currency. So one user can have multiple stripe_customer accounts, one for each currency.
router.post('/create-checkout-session', authenticate.verifyUser, async (req, res) => {
const { currency } = req.body;
...
// If user does not have stripe customer for order currency, create a new one.
if (!(req.user.stripe_customer && req.user.stripe_customer[currency])) {
const new_stripe_customer = await stripe.customers.create({
email: req.user.email,
metadata: {
user_id: req.user._id.toString(),
},
});
let update = {};
update[`stripe_customer.${currency}`] = new_stripe_customer.id;
await Users.findByIdAndUpdate(req.user._id, update);
if (!req.user.stripe_customer) {
req.user.stripe_customer = {};
req.user.stripe_customer[currency] = new_stripe_customer.id;
} else {
req.user.stripe_customer[currency] = new_stripe_customer.id;
}
}
...
// Set `stripe_customer` for checkout session.
const session = await stripe.checkout.sessions.create({
customer: req.user.stripe_customer[currency],
...
});
...
}

Having issues with app.post, results in Cannot Get /

I'm trying to create a sign-up page where users can enter information, however, I'm having issues with "app.post" properly working. This is what I have so far:
const express = require("express");
const db = require("./dbConnectExec.js")
const app = express();
app.use(express.json());
app.listen(5000, () => {
console.log('App is running on port 5000');
});
app.get("/hi", (req,res) => {
res.send("Hello world.");
});
app.get("/", (req,res) => {
res.send("API is running.");
});
// app.post();
// app.put();
app.post("/customers", async(req, res) => {
// res.send("/contacts called");
// console.log("request body", req.body)
let nameFirst = req.body.nameFirst;
let nameLast = req.body.nameLast;
let email = req.body.email;
let password = req.body.password;
let emailCheckQuery = `
SELECT customer_email
FROM Customer
WHERE customer_email = '${email}'`;
let existingUser = await db.executeQuery(emailCheckQuery);
console.log("existing user", existingUser);
if(existingUser[0]){return res.status(409).send("duplicate email")};
})
When I attempt to add a user through Postman, for example:
{"nameFirst": "Robert",
"nameLast": "Redford",
"email": "rob#mail.com",
"password": "asdfasdf"}
I end up with "Cannot GET /customers"
You have no GET handler for /customers only a POST handler
In postman you can change your request from a GET to a POST request. Once you do it should hit this route endpoint.
note:
"GET is used for viewing something, without changing it, while POST is used for changing something. For example, a search page should use GET to get data while a form that changes your password should use POST . Essentially GET is used to retrieve remote data, and POST is used to insert/update remote data."

How can I email links work on the front end?

I have built some functionality into my API that sends a verification email to a user and then when a get request is made to the route the user then becomes active and can be used to login. In order to make this work on the front end I have used a res.redirect which takes me to the login page.
The problem, however, is that this means that when I want to replicate this journey in postman I then receive a load of html rather than a formatted json response. So my question is, is there a way that we can handle this request so that, depending on where it is called, a different response is sent back? As I do not think an event listener will work in the case of an email.
For context, my application uses nodejs, mongodb and pug templates.
`exports.signup = catchAsync(async (req, res, next) => {
const { token } = req.params;
const { email } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findOne({ email });
if (!user || !((await user.signUpId) === token)) {
return next(new AppError('Invalid link', 401));
}
user.ready = true;
user.singUpId = undefined;
await user.save({ validateBeforeSave: false });
const url = `${req.protocol}://${req.get('host')}/me`;
await new Email(user, url).sendWelcome();
await res.redirect('http://localhost:5000/login');
});`

Categories