Add the business plans in.
This commit is contained in:
parent
1cd4fa8d9b
commit
b11ab44285
6 changed files with 145 additions and 46 deletions
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import itertools
|
||||||
|
|
||||||
USER_PLANS = [
|
USER_PLANS = [
|
||||||
{
|
{
|
||||||
|
@ -27,21 +28,60 @@ USER_PLANS = [
|
||||||
'price': 2200,
|
'price': 2200,
|
||||||
'privateRepos': 20,
|
'privateRepos': 20,
|
||||||
'stripeId': 'medium',
|
'stripeId': 'medium',
|
||||||
'audience': 'For medium-sized teams',
|
'audience': 'For medium teams',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Large',
|
||||||
|
'price': 5000,
|
||||||
|
'privateRepos': 50,
|
||||||
|
'stripeId': 'large',
|
||||||
|
'audience': 'For larger teams',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
BUSINESS_PLANS = [
|
||||||
|
{
|
||||||
|
'title': 'Open Source',
|
||||||
|
'price': 0,
|
||||||
|
'privateRepos': 0,
|
||||||
|
'stripeId': 'bus-free',
|
||||||
|
'audience': 'Committment to FOSS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Skiff',
|
||||||
|
'price': 2500,
|
||||||
|
'privateRepos': 10,
|
||||||
|
'stripeId': 'bus-micro',
|
||||||
|
'audience': 'For startups',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Yacht',
|
||||||
|
'price': 5000,
|
||||||
|
'privateRepos': 20,
|
||||||
|
'stripeId': 'bus-small',
|
||||||
|
'audience': 'For small businesses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Freighter',
|
||||||
|
'price': 10000,
|
||||||
|
'privateRepos': 50,
|
||||||
|
'stripeId': 'bus-medium',
|
||||||
|
'audience': 'For normal businesses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Tanker',
|
||||||
|
'price': 20000,
|
||||||
|
'privateRepos': 125,
|
||||||
|
'stripeId': 'bus-large',
|
||||||
|
'audience': 'For large businesses',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def getPlan(id):
|
def get_plan(id):
|
||||||
""" Returns the plan with the given ID or None if none. """
|
""" Returns the plan with the given ID or None if none. """
|
||||||
for plan in USER_PLANS:
|
for plan in itertools.chain(USER_PLANS, BUSINESS_PLANS):
|
||||||
if plan['stripeId'] == id:
|
if plan['stripeId'] == id:
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def isPlanActive(stripe_subscription):
|
|
||||||
""" Returns whether the plan is active. """
|
|
||||||
# TODO: this.
|
|
||||||
return True
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import storage
|
||||||
from data import model
|
from data import model
|
||||||
from data.userfiles import UserRequestFiles
|
from data.userfiles import UserRequestFiles
|
||||||
from data.queue import dockerfile_build_queue
|
from data.queue import dockerfile_build_queue
|
||||||
from data.plans import USER_PLANS, getPlan, isPlanActive
|
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
from util.email import send_confirmation_email, send_recovery_email
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -50,13 +50,19 @@ def api_login_required(f):
|
||||||
def handle_dme(ex):
|
def handle_dme(ex):
|
||||||
return make_response(ex.message, 400)
|
return make_response(ex.message, 400)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/')
|
@app.route('/api/')
|
||||||
def welcome():
|
def welcome():
|
||||||
return make_response('welcome', 200)
|
return make_response('welcome', 200)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/plans/')
|
@app.route('/api/plans/')
|
||||||
def plans_list():
|
def plans_list():
|
||||||
return jsonify({ 'plans': USER_PLANS })
|
return jsonify({
|
||||||
|
'user': USER_PLANS,
|
||||||
|
'business': BUSINESS_PLANS,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['GET'])
|
@app.route('/api/user/', methods=['GET'])
|
||||||
def get_logged_in_user():
|
def get_logged_in_user():
|
||||||
|
@ -112,6 +118,7 @@ def change_user_details():
|
||||||
'askForPassword': user.password_hash is None,
|
'askForPassword': user.password_hash is None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['POST'])
|
@app.route('/api/user/', methods=['POST'])
|
||||||
def create_user_api():
|
def create_user_api():
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
@ -251,6 +258,7 @@ def team_view(orgname, t):
|
||||||
'role': role
|
'role': role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>', methods=['GET'])
|
@app.route('/api/organization/<orgname>', methods=['GET'])
|
||||||
def get_organization(orgname):
|
def get_organization(orgname):
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
|
@ -292,8 +300,8 @@ def get_organization_private_allowed(orgname):
|
||||||
if organization.stripe_id:
|
if organization.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||||
|
|
||||||
if cus.subscription and isPlanActive(cus.subscription):
|
if cus.subscription:
|
||||||
repos_allowed = getPlan(cus.subscription.plan.id)
|
repos_allowed = get_plan(cus.subscription.plan.id)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'privateAllowed': (private_repos < repos_allowed)
|
'privateAllowed': (private_repos < repos_allowed)
|
||||||
})
|
})
|
||||||
|
|
|
@ -309,24 +309,46 @@
|
||||||
color: #428bca;
|
color: #428bca;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plans .all-plans .business-feature {
|
||||||
|
color: #46ac39;
|
||||||
|
}
|
||||||
|
|
||||||
.plans-list {
|
.plans-list {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plans-list .plan-container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.plans-list .plan {
|
.plans-list .plan {
|
||||||
width: 245px;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-right: 10px;
|
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
border-top: 4px solid #94C9F7;
|
border-top: 4px solid #94C9F7;
|
||||||
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.small {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top: 4px solid #428bca;
|
||||||
|
margin-top: 0px;
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.business-plan {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-top: 4px solid #94F794;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.bus-small {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top: 4px solid #47A447;
|
||||||
|
margin-top: 0px;
|
||||||
|
font-size: 1.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plans-list .plan:last-child {
|
.plans-list .plan:last-child {
|
||||||
|
@ -349,7 +371,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-price:after {
|
.plan-price:after {
|
||||||
content: "/ month";
|
content: "/ mo";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
@ -365,6 +387,10 @@
|
||||||
color: #428bca;
|
color: #428bca;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.business-plan .count b {
|
||||||
|
color: #46ac39;
|
||||||
|
}
|
||||||
|
|
||||||
.plans-list .plan .description {
|
.plans-list .plan .description {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -386,14 +412,6 @@
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.plans-list .plan.small {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-top: 4px solid #428bca;
|
|
||||||
margin-top: 0px;
|
|
||||||
font-size: 1.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plans .plan-faq dd{
|
.plans .plan-faq dd{
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,15 +119,19 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
var getPlans = Restangular.one('plans');
|
var getPlans = Restangular.one('plans');
|
||||||
getPlans.get().then(function(data) {
|
getPlans.get().then(function(data) {
|
||||||
for(var i = 0; i < data.plans.length; i++) {
|
var i = 0;
|
||||||
planDict[data.plans[i].stripeId] = data.plans[i];
|
for(i = 0; i < data.user.length; i++) {
|
||||||
|
planDict[data.user[i].stripeId] = data.user[i];
|
||||||
}
|
}
|
||||||
plans = data.plans;
|
for(i = 0; i < data.business.length; i++) {
|
||||||
|
planDict[data.business[i].stripeId] = data.business[i];
|
||||||
|
}
|
||||||
|
plans = data;
|
||||||
callback(plans);
|
callback(plans);
|
||||||
}, function() { callback([]); });
|
}, function() { callback([]); });
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getPlanList = function(callback) {
|
planService.getPlans = function(callback) {
|
||||||
planService.verifyLoaded(callback);
|
planService.verifyLoaded(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -137,10 +141,15 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getMinimumPlan = function(privateCount, callback) {
|
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
|
||||||
planService.verifyLoaded(function() {
|
planService.verifyLoaded(function() {
|
||||||
for (var i = 0; i < plans.length; i++) {
|
var planSource = plans.user;
|
||||||
var plan = plans[i];
|
if (isBusiness) {
|
||||||
|
planSource = plans.business;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < planSource.length; i++) {
|
||||||
|
var plan = planSource[i];
|
||||||
if (plan.privateRepos >= privateCount) {
|
if (plan.privateRepos >= privateCount) {
|
||||||
callback(plan);
|
callback(plan);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -131,7 +131,7 @@ function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserSe
|
||||||
|
|
||||||
function PlansCtrl($scope, UserService, PlanService) {
|
function PlansCtrl($scope, UserService, PlanService) {
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
PlanService.getPlanList(function(plans) {
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.plans = plans;
|
$scope.plans = plans;
|
||||||
$scope.status = 'ready';
|
$scope.status = 'ready';
|
||||||
});
|
});
|
||||||
|
@ -673,8 +673,8 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
|
|
||||||
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
PlanService.getPlanList(function(plans) {
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.plans = plans;
|
$scope.plans = plans.user;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
|
|
@ -7,15 +7,39 @@
|
||||||
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
|
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="plans-list">
|
<div class="row plans-list">
|
||||||
<div class="plan" ng-repeat="plan in plans" ng-class="plan.stripeId">
|
<div class="col-xs-0 col-lg-1"></div>
|
||||||
<div class="plan-title">{{ plan.title }}</div>
|
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.user">
|
||||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
<div class="plan" ng-class="plan.stripeId">
|
||||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
<div class="plan-title">{{ plan.title }}</div>
|
||||||
<div class="description">{{ plan.audience }}</div>
|
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||||
<div class="smaller">SSL secured connections</div>
|
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||||
|
<div class="description">{{ plan.audience }}</div>
|
||||||
|
<div class="smaller">SSL secured connections</div>
|
||||||
|
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
|
<div class="callout">
|
||||||
|
Business Plan Pricing
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="all-plans">
|
||||||
|
All business plans include all of the personal plan features, plus: <span class="business-feature">organizations</span> and <span class="business-feature">teams</span> with <span class="business-feature">delegated access</span> to the organization. All business plans have a <span class="business-feature">14-day free trial</span>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row plans-list">
|
||||||
|
<div class="col-xs-0 col-lg-1"></div>
|
||||||
|
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.business">
|
||||||
|
<div class="plan business-plan" ng-class="plan.stripeId">
|
||||||
|
<div class="plan-title">{{ plan.title }}</div>
|
||||||
|
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||||
|
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||||
|
<div class="description">{{ plan.audience }}</div>
|
||||||
|
<div class="smaller">SSL secured connections</div>
|
||||||
|
<button class="btn btn-success btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Reference in a new issue