After a customer makes a payment, their cookie session gets cleared. This results in them getting sent back to the home page after a successful redirect since I use cookies to verify what step a customer is at to prevent skips. I cannot reproduce this issue. My colleagues cannot reproduce this issue. However, nearly 100% of payments that go through when I'm not observing result in a cleared cookie session.
Additional context:
- I use Node.js.
- I host my service on AWS Lambda.
- I use Chargebee + Stripe to handle payments.
I've observed this issue with the following browser/device combinations (user agent info):
- Chrome 95.0.4638 / Mac OS X 10.15.7
- Chrome 96.0.4664 / Windows 10.0.0
- Chrome Mobile 96.0.4664 / Android 0.0.0
- Mobile Safari 15.1.0 / iOS 15.1.1
- Safari 14.1.2 / Mac OS X 10.15.6
- Chrome 96.0.4664 / Mac OS X 10.14.6
I've tested this on user agents: Chrome 96.0.4664 / Mac OS X 10.15.7, and Safari 15.1.0 / Mac OS X 10.15.7. I did not verify the user agents of my colleagues, but I know it's been tested on mobile and Windows as well, with no issues.
I've also observed this issue with payment methods originating from multiple countries (including the U.S.) and on both Debit and Credit Cards.
Here's relevant code snippet 1 (app.js, I instantiate a cookie session where I store info about the user). I tried adding sameSite: 'none' after reading other posts that this might solve the issue (see: browser clears session cookies), but the issue persisted.
app.use(cookieSession({
name: 'session', // name of the cookie
keys: {defined some keys}, // used to generate signed cookie value.
cookie: {
sameSite: 'none', // possibly required for re-directing after payment gateway is used?
secure: true, // set to true for production. Cookie will only be used on HTTPS domains.
httpOnly: true, // prevents users from accessing cookies from client side javascript
signed: true // appends cookie with a .sig suffix (cookie is signed)
}
}));
That being said, I don't see SameSite=None in the browser when clicking on the cookie. See image below:
Here's relevant code snippet 2 (use.pug, page that opens a Chargebee modal for payments). This is where I instantiate the payment modal and define what happens on successful payment. Notice I redirect the user to a /basicinfo route on success:
script.
function sendDataToServer(data) {
let appEnv = 'prod';
if (appEnv === 'prod'){
window.location = `https://{site_url}/basicinfo?hostedPage=${data}`;
}
}
document.addEventListener("DOMContentLoaded", function() {
// Open chargebee instance
let cbInstance = Chargebee.getInstance();
// Open cart info
let cart = cbInstance.getCart();
let customer = {}; // create customer object
customer["cf_u_id"] = '!{ID}'; // assign customer information
cart.setCustomer(customer); // set customer information
cbInstance.setCheckoutCallbacks(function(cart) {
return {
loaded: function() {
},
close: function() {
},
success: function(hostedPageId) {
sendDataToServer(hostedPageId);
// Hosted page id will be unique token for the checkout that happened
// You can pass this hosted page id to your backend
// and then call our retrieve hosted page api to get subscription details
// https://apidocs.chargebee.com/docs/api/hosted_pages#retrieve_a_hosted_page
},
step: function(value) {
}
}
});
});
Here's relevant code snippet 3 (app.js, where GET basicinfo route is defined). Essentially, what I do here is check that the payment went through correctly, update the cookie as a result, and then render the basicinfo page assuming success:
app.get(app.locals.ep.basicinfo, csrfProtection, (req, res) => {
console.log(`New cookie session? ${req.session.isNew}`);
console.log(`Get BasicInfo Form Step: ${req.session.formStep}`);
console.log(`Get BasicInfo Session Cookie: ${JSON.stringify(req.session)}`, '\n');
console.log(`Full ID: ${req.session.ID}`, '\n');
async function track6(){
if(req.session.refAns.doc !== {criteria}){ // everyone with bug meets criteria
// check to see that payment succeeded
let urlData = url.parse(req.url,true).query; // parse the returned URL
let hostedPage = urlData.hostedPage; // get the hosted page ID created by chargebee
console.log(`Hosted Page: ${hostedPage}`);
await new Promise((resolve, reject) => {
chargebee.hosted_page.retrieve(`${hostedPage}`).request(function(error,result) {
if(error){
console.log(`Hosted page retrieval error: ${error}`);
req.session.formStep = formStep.REFERRAL; // make sure cookie is referral
resolve();
} else {
let createdAt = result.hosted_page.created_at;
console.log(`Hosted page created at UTC time(s): ${createdAt}`);
let invoiceStatus = result.hosted_page.content.invoice.status;
console.log(`Invoice status: ${invoiceStatus}`, '\n');
let presentTime = Date.now()/1000; // UTC time in seconds
console.log(`Present UTC time(s): ${presentTime}`);
let timeSinceCreate = presentTime - createdAt; // in seconds
console.log(`Days since hosted page was created: ${timeSinceCreate/86400}`);
if(invoiceStatus === 'paid' && timeSinceCreate <= 259200){ // paid and no more than 3 days
req.session.formStep = formStep.ACKNOWUSE; // update the cookie
req.session.payment = 'success';
resolve();
} else {
req.session.formStep = formStep.REFERRAL; // make sure cookie is referral
resolve();
}
}
});
});
}
if(req.session.formStep !== formStep.ACKNOWUSE && req.session.formStep !== formStep.BASICINFO && req.session.formStep !== formStep.EVERIFIED){ // if patient hasn't visited the use page and hasn't been everified
res.redirect(app.locals.sp.use); // redirect to the use page
} else {
let wfPage = serverAuth.webflow.style;
if(req.session.botStatus === false) {
await new Promise((resolve, reject) => {
mixpanel.track('Reached BasicInfo', {
distinct_id: req.session.ID
}, function (error) {
console.log(`Track Error: ${error}`, '\n');
resolve();
});
});
}
res.render('basicinfo', {
wfPage: wfPage,
payment: req.session.payment,
csrfToken: req.csrfToken() // pass the csrfToken to the view
});
}
}
track6();
});
What I expect, and what I observe in testing, is that the user's cookie will be preserved, I'll check to see that payment succeeded by retrieving payment data from Chargebee, I'll update the cookie accordingly, and then I'll render the basicinfo page.
Instead, what I observe is that req.session.isNew = true (should be false, is false when I test), req.session.formStep is undefined (should be 'referral', is 'referral' when I test), and req.session.refAns is undefined (should exist, exists when I test). As a result, the site crashes at line: 'if(req.session.refAns.doc !== {criteria}){', since req.session.refAns is undefined. req.session.refAns should not be undefined, since it is defined in a prior site step and stored in the user's cookie. In all cases where this bug has occurred, when I read the users cookie data, up to and including the use route where the payment is made, req.session.refAns is defined.
So my question is this: why is the user's cookie getting deleted after they successfully pay? And why can't I recreate this issue? I know the cookie size is not exceeding 4000 bytes. What I typically see in my own testing is a cookie size of about 400 bytes when the bug happens.
Strangely, some people have found a workaround all on their own by attempting to pay multiple times. Sometimes on the 2nd or 3rd attempt it works and the cookie doesn't get cleared.
Via Active questions tagged javascript - Stack Overflow https://ift.tt/2FdjaAW
Comments
Post a Comment