Add a visible free plan. Tweak the plans and pricing page. Move all plans to a central plans service to have a single point for editing. Support the free plan on the user admin page. Tweak the landing page.

This commit is contained in:
yackob03 2013-10-04 14:35:51 -04:00
parent 8d40f12165
commit 3eca5f65e1
7 changed files with 194 additions and 139 deletions

View file

@ -383,41 +383,43 @@ def subscribe():
else:
# Change the plan
cus = stripe.Customer.retrieve(user.stripe_id)
cus.plan = plan
# User may have been a previous customer who is resubscribing
if 'token' in request_data:
cus.card = request_data['token']
if plan == 'free':
cus.cancel_subscription()
cus.save()
cus.save()
return jsonify(subscription_view(cus.subscription, private_repos))
response_json = {
'plan': 'free',
'usedPrivateRepos': private_repos,
}
else:
cus.plan = plan
# User may have been a previous customer who is resubscribing
if 'token' in request_data:
cus.card = request_data['token']
cus.save()
response_json = subscription_view(cus.subscription, private_repos)
return jsonify(response_json)
@app.route('/api/user/plan', methods=['GET'])
@api_login_required
def get_subscription():
user = current_user.db_user
private_repos = model.get_private_repo_count(user.username)
if user.stripe_id:
private_repos = model.get_private_repo_count(user.username)
cus = stripe.Customer.retrieve(user.stripe_id)
if cus.subscription:
return jsonify(subscription_view(cus.subscription, private_repos))
abort(404)
@app.route('/api/user/plan', methods=['DELETE'])
@api_login_required
def cancel_subscription():
user = current_user.db_user
if user.stripe_id:
private_repos = model.get_private_repo_count(user.username)
cus = stripe.Customer.retrieve(user.stripe_id)
cus.cancel_subscription()
return make_response('Deleted', 204)
abort(404)
return jsonify({
'plan': 'free',
'usedPrivateRepos': private_repos,
});

View file

@ -5,13 +5,23 @@
margin-bottom: 30px;
}
.plans .all-plans {
font-size: 16px;
text-align: center;
margin-bottom: 25px;
}
.plans .all-plans .feature {
color: #428bca;
}
.plans-list {
text-align: center;
margin-bottom: 25px;
}
.plans-list .plan {
width: 245px;
height: 260px;
vertical-align: top;
display: inline-block;
@ -67,16 +77,26 @@
margin-bottom: 30px;
}
.plans-list .plan.focus {
.plans-list .plan .features {
font-size: 16px;
text-align: left;
margin-bottom: 10px;
}
.plans-list .plan .features i {
margin-right: 5px;
}
.plans-list .plan.small {
border: 1px solid #ddd;
border-top: 4px solid #428bca;
margin-top: 0px;
font-size: 1.6em;
height: 270px;
}
.plans-list .plan button {
.plans .plan-faq dd{
margin-bottom: 20px;
}
.loading {
@ -608,6 +628,10 @@ p.editable:hover i {
text-align: center;
}
.user-admin .panel-plan .button-hidden {
visibility: hidden;
}
.user-admin .plan-description {
font-size: 1.2em;
margin-bottom: 10px;

View file

@ -34,7 +34,58 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
userService.load();
return userService;
}])
}]);
$provide.factory('PlanService', [function() {
var plans = [
{
title: 'Open Source',
price: 0,
privateRepos: 0,
stripeId: 'free',
audience: 'Share with the world',
},
{
title: 'Micro',
price: 700,
privateRepos: 5,
stripeId: 'micro',
audience: 'For smaller teams',
},
{
title: 'Basic',
price: 1200,
privateRepos: 10,
stripeId: 'small',
audience: 'For your basic team',
},
{
title: 'Medium',
price: 2200,
privateRepos: 20,
stripeId: 'medium',
audience: 'For medium-sized teams',
},
];
var planDict = {};
var i;
for(i = 0; i < plans.length; i++) {
planDict[plans[i].stripeId] = plans[i];
}
var planService = {}
planService.planList = function() {
return plans;
}
planService.getPlan = function(planId) {
return planDict[planId];
}
return planService;
}]);
}).
directive('match', function($parse) {
return {

View file

@ -77,7 +77,9 @@ function HeaderCtrl($scope, UserService) {
});
}
function PlansCtrl($scope, UserService) {
function PlansCtrl($scope, UserService, PlanService) {
$scope.plans = PlanService.planList();
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
@ -440,38 +442,18 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
});
}
function UserAdminCtrl($scope, Restangular) {
$scope.plans = [
{
title: 'Micro',
price: 700,
privateRepos: 5,
stripeId: 'micro',
},
{
title: 'Basic',
price: 1200,
privateRepos: 10,
stripeId: 'small',
},
{
title: 'Medium',
price: 2200,
privateRepos: 20,
stripeId: 'medium',
},
];
var planDict = {};
var i;
for(i = 0; i < $scope.plans.length; i++) {
planDict[$scope.plans[i].stripeId] = $scope.plans[i];
}
function UserAdminCtrl($scope, Restangular, PlanService, $routeParams) {
$scope.plans = PlanService.planList();
var subscribedToPlan = function(sub) {
$scope.subscription = sub;
$scope.subscribedPlan = planDict[sub.plan];
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrate your subscription to avoid disruptions in your service.';
}
$scope.planLoading = false;
$scope.planChanging = false;
}
@ -505,8 +487,7 @@ function UserAdminCtrl($scope, Restangular) {
});
};
console.log('Got request for plan: ' + planId);
var planDetails = planDict[planId]
var planDetails = PlanService.getPlan(planId)
StripeCheckout.open({
key: 'pk_test_uEDHANKm9CHCvVa2DLcipGRh',
address: false, // TODO change to true
@ -536,34 +517,14 @@ function UserAdminCtrl($scope, Restangular) {
};
$scope.cancelSubscription = function() {
$scope.planChanging = true;
$scope.errorMessage = undefined;
var unsubscribeRequest = Restangular.one('user/plan');
unsubscribeRequest.customDELETE().then(function() {
$scope.subscription = undefined;
$scope.subscribedPlan = undefined;
$scope.planUsagePercent = 0;
$scope.planChanging = false;
}, function() {
// Failure
$scope.errorMessage = 'Unable to unsubscribe.';
$scope.planChanging = false;
});
$scope.changeSubscription('free');
};
// Show the subscribe dialog if a plan was requested.
if (location.hash.indexOf('?') >= 0) {
var query = location.hash.substr(1).split('?')[1];
var data = query.split("&");
for (var i = 0; i < data.length; i++) {
var item = data[i].split("=");
if (item[0] == 'plan') {
var planId = item[1];
if (planDict[planId]) {
$scope.subscribe(planId);
}
break;
}
}
var requested = $routeParams['plan']
if (requested !== undefined && requested !== 'free') {
if (PlanService.getPlan(requested) !== undefined) {
$scope.subscribe(requested);
}
}
}

View file

@ -5,7 +5,7 @@
<div ng-show="user.anonymous">
<h1>Secure hosting for <b>private</b> docker containers</h1>
<h3>Use the docker images <b>your team</b> needs with the safety of <b>private</b> storage</h3>
<div class="sellcall"><a href="#/plans">Starting at $7/mo</a></div>
<div class="sellcall"><a href="#/plans">Private repository plans starting at $7/mo</a></div>
</div>
<div ng-show="!user.anonymous">
@ -41,7 +41,10 @@
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatePassword" match="newUser.password" required>
<button class="btn btn-lg btn-primary btn-block" ng-disabled="signupForm.$invalid" type="submit">Get Started!</button>
<div class="form-group">
<button class="btn btn-lg btn-primary btn-block" ng-disabled="signupForm.$invalid" type="submit">Sign Up for Free!</button>
<p class="help-block">No credit card required.</p>
</div>
</form>
<div ng-show="registering" style="text-align: center">
<span class="spin" color="#fff" style="display: inline-block"></span>
@ -67,15 +70,15 @@
</div>
<div class="col-md-4 shoutout">
<i class="icon-cloud"></i>
<b>Cloud Hosted</b>
Accessible from anywhere, anytime
<i class="icon-user"></i>
<b>Shareable</b>
Have to share a container? No problem! Share with anyone you choose
</div>
<div class="col-md-4 shoutout">
<i class="icon-share-sign"></i>
<b>Shareable</b>
Have to share a container? No problem! Share with anyone you choose
<i class="icon-cloud"></i>
<b>Cloud Hosted</b>
Accessible from anywhere, anytime
</div>
</div> <!-- row -->
</div> <!-- container -->

View file

@ -1,52 +1,59 @@
<div class="container plans">
<div class="callout">
Plans and Pricing
Plans &amp; Pricing
</div>
<div class="all-plans">
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 class="plans-list">
<div class="plan">
<div class="plan-title">Micro</div>
<div class="plan-price">$7</div>
<div class="count"><b>5</b> private repositories</div>
<div class="description">For smaller teams</div>
<div class="plan" ng-repeat="plan in plans" 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>
<button class="btn btn-primary btn-block" ng-click="buyNow('micro')">Buy Now</button>
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
</div>
</div>
<div class="row plan-faq">
<div class="col-md-6">
<dl>
<dt>Can I use Quay for free?</dt>
<dd>Yes! We offer unlimited storage and serving of public repositories. We strongly believe in the open source community and will do what we can to help!</dd>
<dt>What types of payment do you accept?</dt>
<dd>Quay uses Stripe as our payment processor, so we can accept any of the payment options they offer, which are currently: Visa, MasterCard, American Express, JCB, Discover and Diners Club.</dd>
</dl>
</div>
<div class="col-md-6">
<dl>
<dt>Can I change my plan?</dt>
<dd>Yes, you can change your plan at any time and your account will be pro-rated for the difference.</dd>
<dt>Do you offer special plans for business or academic institutions?</dt>
<dd>Please contact us at our <a href="mailto:support@quay.io">support email address</a> to discuss the details of your organization and inteded usage.</dd>
</dl>
</div>
<div class="plan focus">
<div class="plan-title">Basic</div>
<div class="plan-price">$12</div>
<div class="count"><b>10</b> private repositories</div>
<div class="description">For your basic team</div>
<button class="btn btn-primary btn-block" ng-click="buyNow('small')">Buy Now</button>
</div>
<div class="plan">
<div class="plan-title">Medium</div>
<div class="plan-price">$22</div>
<div class="count"><b>20</b> private repositories</div>
<div class="description">For medium-sized teams</div>
<button class="btn btn-primary btn-block" ng-click="buyNow('medium')">Buy Now</button>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="signinModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Please Sign In</h4>
</div>
<div class="modal-body">
Please sign into Quay in order to continue
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="signinModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Please Sign In</h4>
</div>
<div class="modal-body">
Please sign into Quay in order to continue
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -8,7 +8,7 @@
</div>
</div>
<div class="row" ng-hide="planLoading">
<div class="col-md-4" ng-repeat='plan in plans'>
<div class="col-md-3" ng-repeat='plan in plans'>
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
<div class="panel-heading">
{{ plan.title }}
@ -20,9 +20,16 @@
<div class="panel-body panel-plan">
<div class="plan-price">${{ plan.price / 100 }}</div>
<div class="plan-description"><b>{{ plan.privateRepos }}</b> Private Repositories</div>
<button class="btn btn-primary" ng-hide="subscription" ng-click="subscribe(plan.stripeId)">Subscribe</button>
<button class="btn" ng-show="subscription && (subscription.plan != plan.stripeId)" ng-click="changeSubscription(plan.stripeId)">Change</button>
<button class="btn btn-danger" ng-show="subscription.plan == plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
<div ng-switch='plan.stripeId'>
<div ng-switch-when='free'>
<button class="btn button-hidden">Hidden!</button>
</div>
<div ng-switch-default>
<button class="btn btn-primary" ng-show="subscription.plan === 'free'" ng-click="subscribe(plan.stripeId)">Subscribe</button>
<button class="btn btn-default" ng-hide="subscription.plan === 'free' || subscription.plan === plan.stripeId" ng-click="changeSubscription(plan.stripeId)">Change</button>
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
</div>
</div>
</div>
</div>
</div>