From 0f430e102e9beaa504ecd8adc53d305412de609d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Mar 2014 20:00:11 -0600 Subject: [PATCH] feat(subscriptions): add paypal recurring cancelation. refactor stripe & paypal code to use auth.authWithUrl. --- config.json.example | 1 - package.json | 3 +- public/js/controllers/rootCtrl.js | 8 ++--- src/controllers/auth.js | 13 +++++--- src/controllers/user.js | 49 +++++++++++++++++++++++-------- src/middleware.js | 1 - src/models/user.js | 1 + src/routes/apiv2.coffee | 10 ------- src/routes/pages.js | 9 ++++-- views/options/settings.jade | 6 ++-- views/shared/modals/buy-gems.jade | 7 +++-- 11 files changed, 65 insertions(+), 43 deletions(-) diff --git a/config.json.example b/config.json.example index 0e0b3ff2cc..ccf97ff489 100644 --- a/config.json.example +++ b/config.json.example @@ -16,7 +16,6 @@ "SMTP_TLS": true, "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", "STRIPE_PUB_KEY":"22223333444455556666777788889999", - "PAYPAL_MERCHANT":"paypal-merchant@gmail.com", "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY", "GA_ID": "GA_ID", "PAYPAL_USERNAME": "PAYPAL_USERNAME", diff --git a/package.json b/package.json index 7cebd45d2e..20dd2b1602 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "validator": "~1.5.1", "nodemailer": "~0.5.2", "grunt-cli": "~0.1.9", - "paypal-ipn": "~1.0.1", "express-csv": "~0.6.0", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "js2xmlparser": "~0.1.2", @@ -51,7 +50,7 @@ "domain-middleware": "~0.1.0", "universal-analytics": "~0.3.2", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", - "paypal-recurring": "~1.1.0" + "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca" }, "private": true, "subdomain": "habitrpg", diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index a7b2abe24f..a8ec1ea33f 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -107,9 +107,8 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ window.env.t('donationDesc'), panelLabel: subscription ? window.env.t('subscribe') : window.env.t('checkout'), token: function(data) { - var url = '/api/v2/user/buy-gems'; + var url = '/stripe/checkout'; if (subscription) url += '?plan=basic_earned'; -// if (subscription) url += '?plan=test'; $scope.$apply(function(){ $http.post(url, data).success(function() { window.location.reload(true); @@ -123,10 +122,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $scope.cancelSubscription = function(){ if (!confirm(window.env.t('sureCancelSub'))) return; - //TODO use Stripe API to keep subscription till end of their month - $http.post('/api/v2/user/cancel-subscription').success(function(){ - window.location.reload(true); - }) + window.location.href = '/' + user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + user._id + '&apiToken=' + user.apiToken; } $scope.contribText = function(contrib, backer){ diff --git a/src/controllers/auth.js b/src/controllers/auth.js index eab3cf8c9d..0bb11f7986 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -16,10 +16,6 @@ var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your var NO_USER_FOUND = {err: "No user found."}; var NO_SESSION_FOUND = { err: "You must be logged in." }; -/* - beforeEach auth interceptor - */ - api.auth = function(req, res, next) { var uid = req.headers['x-api-user']; var token = req.headers['x-api-key']; @@ -47,6 +43,15 @@ api.authWithSession = function(req, res, next) { //[todo] there is probably a mo }); }; +api.authWithUrl = function(req, res, next) { + User.findOne({_id:req.query._id, apiToken:req.query.apiToken}, function(err,user){ + if (err) return next(err); + if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); + res.locals.user = user; + next(); + }) +} + api.registerUser = function(req, res, next) { var confirmPassword, e, email, password, username, _ref; _ref = req.body, email = _ref.email, username = _ref.username, password = _ref.password, confirmPassword = _ref.confirmPassword; diff --git a/src/controllers/user.js b/src/controllers/user.js index 9209558793..2296697e6d 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -312,7 +312,8 @@ function createSubscription(user, data) { planId:'basic_earned', customerId: data.customerId, dateUpdated: new Date, - gemsBought: 0 + gemsBought: 0, + paymentMethod: data.paymentMethod }) .defaults({ // allow non-override if a plan was previously used dateCreated: new Date, @@ -324,6 +325,12 @@ function createSubscription(user, data) { ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); } +function cancelSubscription(user, data){ + _.merge(user.purchased.plan, {planId:null, customerId:null, paymentMethod:null}); + user.markModified('purchased.plan'); + ga.event('unsubscribe', 'Stripe').send(); +} + function buyGems(user, data) { user.balance += 5; user.purchased.txnCount++; @@ -334,7 +341,7 @@ function buyGems(user, data) { /* Setup Stripe response when posting payment */ -api.buyGems = function(req, res, next) { +api.stripeCheckout = function(req, res, next) { var api_key = nconf.get('STRIPE_API_KEY'); var stripe = require("stripe")(api_key); var token = req.body.id; @@ -367,13 +374,12 @@ api.buyGems = function(req, res, next) { } ], function(err, saved){ if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors - res.send(200, saved); + res.send(200); }); }; -api.cancelSubscription = function(req, res, next) { - var api_key = nconf.get('STRIPE_API_KEY'); - var stripe = require("stripe")(api_key); +api.stripeSubscribeCancel = function(req, res, next) { + var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); var user = res.locals.user; if (!user.purchased.plan.customerId) return res.json(401, {err: "User does not have a plan subscription"}); @@ -383,20 +389,17 @@ api.cancelSubscription = function(req, res, next) { stripe.customers.del(user.purchased.plan.customerId, cb); }, function(response, cb) { - _.merge(user.purchased.plan, {planId:null, customerId:null}); - user.markModified('purchased.plan'); + cancelSubscription(user); user.save(cb); } ], function(err, saved){ if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors - res.send(200, saved); - ga.event('unsubscribe', 'Stripe').send() + res.redirect('/'); }); } api.paypalSubscribe = function(req,res,next) { - var uuid = req.query.uuid; - if (!uuid) return next("UUID required"); + var uuid = res.locals.user._id; // Authenticate a future subscription of ~5 USD paypalRecurring.authenticate({ RETURNURL: nconf.get('BASE_URL') + '/paypal/subscribe/success?uuid=' + uuid, @@ -432,8 +435,28 @@ api.paypalSubscribeSuccess = function(req,res,next) { }); } +api.paypalSubscribeCancel = function(req, res, next) { + var user = res.locals.user; + if (!user.purchased.plan.customerId) + return res.json(401, {err: "User does not have a plan subscription"}); + async.waterfall([ + function(cb) { + paypalRecurring.modifySubscription(user.purchased.plan.customerId, 'cancel', cb); + }, + function(response, cb) { + cancelSubscription(user); + user.save(cb); + } + ], function(err, saved){ + if (err) return next(err); + res.redirect('/'); + }); + +} + api.paypalCheckout = function(req, res, next) { - var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + req.query.uuid}; + var uuid = res.locals.user._id; + var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + uuid}; paypalCheckout.pay(+new Date, 5, 'HabitRPG Gems', 'USD', opts, function(err, url) { if (err) return next(err); res.redirect(url); diff --git a/src/middleware.js b/src/middleware.js index 4894b586d1..e0125ee748 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -234,7 +234,6 @@ module.exports.locals = function(req, res, next) { res.locals.habitrpg = { NODE_ENV: nconf.get('NODE_ENV'), BASE_URL: nconf.get('BASE_URL'), - PAYPAL_MERCHANT: nconf.get('PAYPAL_MERCHANT'), GA_ID: nconf.get("GA_ID"), IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')), STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'), diff --git a/src/models/user.js b/src/models/user.js index 1efa96afea..b52c8e3df5 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -85,6 +85,7 @@ var UserSchema = new Schema({ mobileChat: Boolean, plan: { planId: String, + paymentMethod: String, //enum: ['Paypal','Stripe', '']} customerId: String, dateCreated: Date, dateUpdated: Date, diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index d51f3f45ef..9cf7dd3190 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -299,16 +299,6 @@ module.exports = (swagger, v2) -> ] action: user.unlock - "/user/buy-gems": - spec: method: 'POST', description: "Do not use this route" - middleware: auth.auth - action:user.buyGems - - "/user/cancel-subscription": - spec: method: 'POST', description: "Do not use this route" - middleware: auth.auth - action:user.cancelSubscription - "/user/batch-update": spec: method: 'POST' diff --git a/src/routes/pages.js b/src/routes/pages.js index 1f0605896c..555aa7a157 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -59,9 +59,14 @@ router.get('/static/extensions', function(req, res) { // --------- PayPal -------- -router.get('/paypal/checkout', user.paypalCheckout); +router.get('/paypal/checkout', auth.authWithUrl, user.paypalCheckout); router.get('/paypal/checkout/success', user.paypalCheckoutSuccess); -router.get('/paypal/subscribe', user.paypalSubscribe); +router.get('/paypal/subscribe', auth.authWithUrl, user.paypalSubscribe); router.get('/paypal/subscribe/success', user.paypalSubscribeSuccess); +router.get('/paypal/subscribe/cancel', auth.authWithUrl, user.paypalSubscribeCancel); + +router.post("/stripe/checkout", auth.auth, user.stripeCheckout); +//router.get("/stripe/subscribe", auth.authWithUrl, user.stripeSubscribe); // checkout route is used (above) with ?plan= instead +router.get("/stripe/subscribe/cancel", auth.authWithUrl, user.stripeSubscribeCancel); module.exports = router; \ No newline at end of file diff --git a/views/options/settings.jade b/views/options/settings.jade index 5b81e25bef..4cc39be9d3 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -145,10 +145,12 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template') h2=env.t('individualSub') div(ng-if='!user.purchased.plan.customerId') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-primary(ng-click='showStripe(true)')=env.t('subscribe') + p + small.muted Payment Methods: + .btn.btn-primary(ng-click='showStripe(true)') Card //-small.muted PayPal coming soon. //a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal - a.btn.btn-warning(href='/paypal/subscribe?uuid={{user._id}}') PayPal + a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal div(ng-if='user.purchased.plan.customerId') .well diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade index 87f4cefc2a..155a90d863 100644 --- a/views/shared/modals/buy-gems.jade +++ b/views/shared/modals/buy-gems.jade @@ -19,8 +19,11 @@ script(id='modals/buyGems.html', type='text/ng-template') =env.t('USD') tr td - .btn.btn-primary(ng-click='showStripe()')=env.t('payWithCard') - a.btn.btn-warning(href='/paypal/checkout?uuid={{user._id}}') PayPal + p + small.muted Payment Methods: + //.btn.btn-primary(ng-click='showStripe()')=env.t('payWithCard') + .btn.btn-primary(ng-click='showStripe()') Card + a.btn.btn-warning(href='/paypal/checkout?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal div(ng-include="'partials/options.settings.subscription.html'")