/** * 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.getPlanImmediately = function(planId) { // Get the plan by name, without bothering to check if the plans are loaded. // This method will return undefined if planId is undefined or null, or if // the planDict has not yet been loaded. return 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, 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', billingAddress: true, zipCode: true, 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, email: email, amount: planDetails.price, currency: 'usd', name: 'Quay ' + planDetails.title + ' Subscription', description: 'Up to ' + planDetails.privateRepos + ' private repositories', panelLabel: opt_title || 'Subscribe', token: submitToken, image: 'static/img/quay-icon-stripe.png', billingAddress: true, zipCode: true, opened: function() { $scope.$apply(function() { callbacks['opened']() }); }, closed: function() { $scope.$apply(function() { callbacks['closed']() }); } }); }); }; return planService; }]);