36cd79c2c0
Fixes Issue #101 We change the Stripe plan subscription code to require a new credit card to be entered every time a user moves from the open source plan to a paid plan. When a customer's credit card fails, Stripe auto-desubscribes the user from an active plan, but (before this change) we would try to resubscribe with the invalid card.
371 lines
No EOL
11 KiB
JavaScript
371 lines
No EOL
11 KiB
JavaScript
/**
|
|
* Helper service for loading, changing and working with subscription plans.
|
|
*/
|
|
angular.module('quay')
|
|
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
|
|
|
|
function(KeyService, UserService, CookieService, ApiService, Features, Config) {
|
|
var plans = null;
|
|
var planDict = {};
|
|
var planService = {};
|
|
var listeners = [];
|
|
|
|
var previousSubscribeFailure = false;
|
|
|
|
planService.getFreePlan = function() {
|
|
return 'free';
|
|
};
|
|
|
|
planService.registerListener = function(obj, callback) {
|
|
listeners.push({'obj': obj, 'callback': callback});
|
|
};
|
|
|
|
planService.unregisterListener = function(obj) {
|
|
for (var i = 0; i < listeners.length; ++i) {
|
|
if (listeners[i].obj == obj) {
|
|
listeners.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
planService.notePlan = function(planId) {
|
|
if (Features.BILLING) {
|
|
CookieService.putSession('quay.notedplan', planId);
|
|
}
|
|
};
|
|
|
|
planService.isOrgCompatible = function(plan) {
|
|
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
|
|
};
|
|
|
|
planService.getMatchingBusinessPlan = function(callback) {
|
|
planService.getPlans(function() {
|
|
planService.getSubscription(null, function(sub) {
|
|
var plan = planDict[sub.plan];
|
|
if (!plan) {
|
|
planService.getMinimumPlan(0, true, callback);
|
|
return;
|
|
}
|
|
|
|
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
|
|
planService.getMinimumPlan(count, true, callback);
|
|
}, function() {
|
|
planService.getMinimumPlan(0, true, callback);
|
|
});
|
|
});
|
|
};
|
|
|
|
planService.handleNotedPlan = function() {
|
|
var planId = planService.getAndResetNotedPlan();
|
|
if (!planId || !Features.BILLING) { return false; }
|
|
|
|
UserService.load(function() {
|
|
if (UserService.currentUser().anonymous) {
|
|
return;
|
|
}
|
|
|
|
planService.getPlan(planId, function(plan) {
|
|
if (planService.isOrgCompatible(plan)) {
|
|
document.location = '/organizations/new/?plan=' + planId;
|
|
} else {
|
|
document.location = '/user?plan=' + planId;
|
|
}
|
|
});
|
|
});
|
|
|
|
return true;
|
|
};
|
|
|
|
planService.getAndResetNotedPlan = function() {
|
|
var planId = CookieService.get('quay.notedplan');
|
|
CookieService.clear('quay.notedplan');
|
|
return planId;
|
|
};
|
|
|
|
planService.handleCardError = function(resp) {
|
|
if (!planService.isCardError(resp)) { return; }
|
|
|
|
bootbox.dialog({
|
|
"message": resp.data.carderror,
|
|
"title": "Credit card issue",
|
|
"buttons": {
|
|
"close": {
|
|
"label": "Close",
|
|
"className": "btn-primary"
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
planService.isCardError = function(resp) {
|
|
return resp && resp.data && resp.data.carderror;
|
|
};
|
|
|
|
planService.verifyLoaded = function(callback) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
if (plans && plans.length) {
|
|
callback(plans);
|
|
return;
|
|
}
|
|
|
|
ApiService.listPlans().then(function(data) {
|
|
plans = data.plans || [];
|
|
for(var i = 0; i < plans.length; i++) {
|
|
planDict[plans[i].stripeId] = plans[i];
|
|
}
|
|
callback(plans);
|
|
}, function() { callback([]); });
|
|
};
|
|
|
|
planService.getPlans = function(callback, opt_includePersonal) {
|
|
planService.verifyLoaded(function(plans) {
|
|
var filtered = [];
|
|
for (var i = 0; i < plans.length; ++i) {
|
|
var plan = plans[i];
|
|
if (plan['deprecated']) { continue; }
|
|
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
|
|
filtered.push(plan);
|
|
}
|
|
callback(filtered);
|
|
});
|
|
};
|
|
|
|
planService.getPlan = function(planId, callback) {
|
|
planService.getPlanIncludingDeprecated(planId, function(plan) {
|
|
if (!plan['deprecated']) {
|
|
callback(plan);
|
|
}
|
|
});
|
|
};
|
|
|
|
planService.getPlanIncludingDeprecated = function(planId, callback) {
|
|
planService.verifyLoaded(function() {
|
|
if (planDict[planId]) {
|
|
callback(planDict[planId]);
|
|
}
|
|
});
|
|
};
|
|
|
|
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
|
|
planService.getPlans(function(plans) {
|
|
for (var i = 0; i < plans.length; i++) {
|
|
var plan = plans[i];
|
|
if (plan.privateRepos >= privateCount) {
|
|
callback(plan);
|
|
return;
|
|
}
|
|
}
|
|
|
|
callback(null);
|
|
}, /* include personal */!isBusiness);
|
|
};
|
|
|
|
planService.getSubscription = function(orgname, success, failure) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
ApiService.getSubscription(orgname).then(success, failure);
|
|
};
|
|
|
|
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
var subscriptionDetails = {
|
|
plan: planId
|
|
};
|
|
|
|
if (opt_token) {
|
|
subscriptionDetails['token'] = opt_token.id;
|
|
}
|
|
|
|
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
|
|
success(resp);
|
|
planService.getPlan(planId, function(plan) {
|
|
for (var i = 0; i < listeners.length; ++i) {
|
|
listeners[i]['callback'](plan);
|
|
}
|
|
});
|
|
}, failure);
|
|
};
|
|
|
|
planService.getCardInfo = function(orgname, callback) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
ApiService.getCard(orgname).then(function(resp) {
|
|
callback(resp.card);
|
|
}, function() {
|
|
callback({'is_valid': false});
|
|
});
|
|
};
|
|
|
|
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
if (callbacks['started']) {
|
|
callbacks['started']();
|
|
}
|
|
|
|
planService.getSubscription(orgname, function(sub) {
|
|
planService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
|
|
planService.changePlanInternal($scope, orgname, planId, callbacks, opt_async,
|
|
subscribedPlan.price > 0);
|
|
});
|
|
}, function() {
|
|
planService.changePlanInternal($scope, orgname, planId, callbacks, opt_async, false);
|
|
});
|
|
};
|
|
|
|
planService.changePlanInternal = function($scope, orgname, planId, callbacks, opt_async,
|
|
opt_reuseCard) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
planService.getPlan(planId, function(plan) {
|
|
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
|
|
|
planService.getCardInfo(orgname, function(cardInfo) {
|
|
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4 || !opt_reuseCard)) {
|
|
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
|
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
|
|
return;
|
|
}
|
|
|
|
previousSubscribeFailure = false;
|
|
|
|
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
|
previousSubscribeFailure = true;
|
|
planService.handleCardError(resp);
|
|
callbacks['failure'](resp);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
planService.changeCreditCard = function($scope, orgname, callbacks) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
if (callbacks['opening']) {
|
|
callbacks['opening']();
|
|
}
|
|
|
|
var submitted = false;
|
|
var submitToken = function(token) {
|
|
if (submitted) { return; }
|
|
submitted = true;
|
|
$scope.$apply(function() {
|
|
if (callbacks['started']) {
|
|
callbacks['started']();
|
|
}
|
|
|
|
var cardInfo = {
|
|
'token': token.id
|
|
};
|
|
|
|
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
|
|
planService.handleCardError(resp);
|
|
callbacks['failure'](resp);
|
|
});
|
|
});
|
|
};
|
|
|
|
var email = planService.getEmail(orgname);
|
|
StripeCheckout.open({
|
|
key: KeyService.stripePublishableKey,
|
|
address: false,
|
|
email: email,
|
|
currency: 'usd',
|
|
name: 'Update credit card',
|
|
description: 'Enter your credit card number',
|
|
panelLabel: 'Update',
|
|
token: submitToken,
|
|
image: 'static/img/quay-icon-stripe.png',
|
|
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
|
|
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
|
|
});
|
|
};
|
|
|
|
planService.getEmail = function(orgname) {
|
|
var email = null;
|
|
if (UserService.currentUser()) {
|
|
email = UserService.currentUser().email;
|
|
|
|
if (orgname) {
|
|
org = UserService.getOrganization(orgname);
|
|
if (org) {
|
|
emaiil = org.email;
|
|
}
|
|
}
|
|
}
|
|
return email;
|
|
};
|
|
|
|
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
|
|
if (!Features.BILLING) { return; }
|
|
|
|
// If the async parameter is true and this is a browser that does not allow async popup of the
|
|
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
|
|
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
|
|
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
|
|
|
|
if (opt_async && (isIE || isMobileSafari)) {
|
|
bootbox.dialog({
|
|
"message": "Please click 'Subscribe' to continue",
|
|
"buttons": {
|
|
"subscribe": {
|
|
"label": "Subscribe",
|
|
"className": "btn-primary",
|
|
"callback": function() {
|
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
|
|
}
|
|
},
|
|
"close": {
|
|
"label": "Cancel",
|
|
"className": "btn-default"
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (callbacks['opening']) {
|
|
callbacks['opening']();
|
|
}
|
|
|
|
var submitted = false;
|
|
var submitToken = function(token) {
|
|
if (submitted) { return; }
|
|
submitted = true;
|
|
|
|
if (Config.MIXPANEL_KEY) {
|
|
mixpanel.track('plan_subscribe');
|
|
}
|
|
|
|
$scope.$apply(function() {
|
|
if (callbacks['started']) {
|
|
callbacks['started']();
|
|
}
|
|
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
|
|
});
|
|
};
|
|
|
|
planService.getPlan(planId, function(planDetails) {
|
|
var email = planService.getEmail(orgname);
|
|
StripeCheckout.open({
|
|
key: KeyService.stripePublishableKey,
|
|
address: false,
|
|
email: email,
|
|
amount: planDetails.price,
|
|
currency: 'usd',
|
|
name: 'Quay.io ' + planDetails.title + ' Subscription',
|
|
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
|
panelLabel: opt_title || 'Subscribe',
|
|
token: submitToken,
|
|
image: 'static/img/quay-icon-stripe.png',
|
|
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
|
|
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
|
|
});
|
|
});
|
|
};
|
|
|
|
return planService;
|
|
}]); |