diff --git a/endpoints/api.py b/endpoints/api.py index 1f1222a92..062ad69e1 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1218,6 +1218,80 @@ def subscription_view(stripe_subscription, used_repos): } +@app.route('/api/user/card', methods=['GET']) +@api_login_required +def get_user_card_api(): + user = current_user.db_user() + return jsonify(get_card(user)) + + +@app.route('/api/organization//card', methods=['GET']) +@api_login_required +def get_org_card_api(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + return jsonify(get_card(organization)) + + abort(403) + + +@app.route('/api/user/card', methods=['POST']) +@api_login_required +def set_user_card_api(): + user = current_user.db_user() + token = request.get_json()['token'] + return jsonify(set_card(user, token)) + + +@app.route('/api/organization//card', methods=['POST']) +@api_login_required +def set_org_card_api(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + token = request.get_json()['token'] + return jsonify(set_card(organization, token)) + + abort(403) + + +def set_card(user, token): + print token + + if user.stripe_id: + cus = stripe.Customer.retrieve(user.stripe_id) + if cus: + cus.card = token + cus.save() + + return get_card(user) + + +def get_card(user): + card_info = { + 'is_valid': False + } + + if user.stripe_id: + cus = stripe.Customer.retrieve(user.stripe_id) + if cus and cus.default_card: + # Find the default card. + default_card = None + for card in cus.cards.data: + if card.id == cus.default_card: + default_card = card + break + + if default_card: + card_info = { + 'owner': card.name, + 'type': card.type, + 'last4': card.last4 + } + + return {'card': card_info} + @app.route('/api/user/plan', methods=['PUT']) @api_login_required def subscribe_api(): diff --git a/static/css/quay.css b/static/css/quay.css index e466e43d6..dcf702505 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -6,9 +6,39 @@ visibility: hidden; } +.billing-options-element .current-card { + font-size: 16px; + margin-bottom: 20px; +} + + +.billing-options-element .current-card .no-card-outline { + display: inline-block; + width: 73px; + height: 44px; + vertical-align: middle; + margin-right: 10px; + border: 1px dashed #aaa; + border-radius: 4px; +} + +.billing-options-element .current-card .last4 { + color: #aaa; +} + +.billing-options-element .current-card .last4 b { + color: black; +} + +.billing-options-element .current-card img { + margin-right: 10px; + vertical-align: middle; +} + .settings-option { padding: 4px; font-size: 18px; + margin-bottom: 10px; } .settings-option label { diff --git a/static/directives/billing-options.html b/static/directives/billing-options.html index 777a56519..93eaf12ca 100644 --- a/static/directives/billing-options.html +++ b/static/directives/billing-options.html @@ -1,4 +1,27 @@ -
+
+ +
+
+ Credit Card +
+
+ +
+ + + + ****-****-****-{{ currentCard.last4 }} + No credit card found +
+ + +
+
+ +
Billing Options diff --git a/static/img/creditcards/amex.png b/static/img/creditcards/amex.png new file mode 100644 index 000000000..492d40aff Binary files /dev/null and b/static/img/creditcards/amex.png differ diff --git a/static/img/creditcards/credit.png b/static/img/creditcards/credit.png new file mode 100644 index 000000000..0b8f49aa7 Binary files /dev/null and b/static/img/creditcards/credit.png differ diff --git a/static/img/creditcards/dankort.png b/static/img/creditcards/dankort.png new file mode 100644 index 000000000..400f57c95 Binary files /dev/null and b/static/img/creditcards/dankort.png differ diff --git a/static/img/creditcards/diners.png b/static/img/creditcards/diners.png new file mode 100644 index 000000000..72e1808fb Binary files /dev/null and b/static/img/creditcards/diners.png differ diff --git a/static/img/creditcards/discover.png b/static/img/creditcards/discover.png new file mode 100644 index 000000000..7e36dd715 Binary files /dev/null and b/static/img/creditcards/discover.png differ diff --git a/static/img/creditcards/forbru.png b/static/img/creditcards/forbru.png new file mode 100644 index 000000000..2668cdb3d Binary files /dev/null and b/static/img/creditcards/forbru.png differ diff --git a/static/img/creditcards/google.png b/static/img/creditcards/google.png new file mode 100644 index 000000000..ab9566bde Binary files /dev/null and b/static/img/creditcards/google.png differ diff --git a/static/img/creditcards/jcb.png b/static/img/creditcards/jcb.png new file mode 100644 index 000000000..9bb0cbafd Binary files /dev/null and b/static/img/creditcards/jcb.png differ diff --git a/static/img/creditcards/laser.png b/static/img/creditcards/laser.png new file mode 100644 index 000000000..2f2417942 Binary files /dev/null and b/static/img/creditcards/laser.png differ diff --git a/static/img/creditcards/maestro.png b/static/img/creditcards/maestro.png new file mode 100644 index 000000000..38fdb4b7d Binary files /dev/null and b/static/img/creditcards/maestro.png differ diff --git a/static/img/creditcards/mastercard.png b/static/img/creditcards/mastercard.png new file mode 100644 index 000000000..af9b1d701 Binary files /dev/null and b/static/img/creditcards/mastercard.png differ diff --git a/static/img/creditcards/money.png b/static/img/creditcards/money.png new file mode 100644 index 000000000..a99814aa1 Binary files /dev/null and b/static/img/creditcards/money.png differ diff --git a/static/img/creditcards/paypa.png b/static/img/creditcards/paypa.png new file mode 100644 index 000000000..4fad9d864 Binary files /dev/null and b/static/img/creditcards/paypa.png differ diff --git a/static/img/creditcards/shopify.png b/static/img/creditcards/shopify.png new file mode 100644 index 000000000..f40146b1b Binary files /dev/null and b/static/img/creditcards/shopify.png differ diff --git a/static/img/creditcards/solo.png b/static/img/creditcards/solo.png new file mode 100644 index 000000000..c236ecc0f Binary files /dev/null and b/static/img/creditcards/solo.png differ diff --git a/static/img/creditcards/visa.png b/static/img/creditcards/visa.png new file mode 100644 index 000000000..ffd17c5a2 Binary files /dev/null and b/static/img/creditcards/visa.png differ diff --git a/static/js/app.js b/static/js/app.js index 087e299e3..e7f1554d1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -122,8 +122,22 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', $provide.factory('PlanService', ['Restangular', 'KeyService', 'UserService', function(Restangular, KeyService, UserService) { var plans = null; var planDict = {}; - var planService = {} + var planService = {}; + var listeners = []; + 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.verifyLoaded = function(callback) { if (plans) { callback(plans); @@ -192,8 +206,8 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', }); }; - planService.getSubscription = function(organization, success, failure) { - var url = planService.getSubscriptionUrl(organization); + planService.getSubscription = function(orgname, success, failure) { + var url = planService.getSubscriptionUrl(orgname); var getSubscription = Restangular.one(url); getSubscription.get().then(success, failure); }; @@ -213,7 +227,14 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', var url = planService.getSubscriptionUrl(orgname); var createSubscriptionRequest = Restangular.one(url); - createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failure); + createSubscriptionRequest.customPUT(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.changePlan = function($scope, orgname, planId, hasExistingSubscription, callbacks) { @@ -228,6 +249,58 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure']); }; + planService.changeCreditCard = function($scope, orgname, callbacks) { + if (callbacks['opening']) { + callbacks['opening'](); + } + + var submitToken = function(token) { + $scope.$apply(function() { + if (callbacks['started']) { + callbacks['started'](); + } + + var cardInfo = { + 'token': token.id + }; + + var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; + var changeCardRequest = Restangular.one(url); + changeCardRequest.customPOST(cardInfo).then(callbacks['success'], callbacks['failure']); + }); + }; + + 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) { if (callbacks['opening']) { callbacks['opening'](); @@ -245,18 +318,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', }; planService.getPlan(planId, function(planDetails) { - var email = null; - if (UserService.currentUser()) { - email = UserService.currentUser().email; - - if (orgname) { - org = UserService.getOrganization(orgname); - if (org) { - emaiil = org.email; - } - } - } - + var email = planService.getEmail(orgname); StripeCheckout.open({ key: KeyService.stripePublishableKey, address: false, @@ -632,13 +694,71 @@ quayApp.directive('billingOptions', function () { 'user': '=user', 'organization': '=organization' }, - controller: function($scope, $element, Restangular) { + controller: function($scope, $element, PlanService, Restangular) { $scope.invoice_email = false; + $scope.currentCard = null; + + // Listen to plan changes. + PlanService.registerListener(this, function(plan) { + if (plan && plan.price > 0) { + update(); + } + }); + + $scope.$on('$destroy', function() { + PlanService.unregisterListener(this); + }); + + $scope.changeCard = function() { + $scope.changingCard = true; + var callbacks = { + 'opened': function() { $scope.changingCard = true; }, + 'closed': function() { $scope.changingCard = false; }, + 'started': function() { $scope.currentCard = null; }, + 'success': function(resp) { + $scope.currentCard = resp.card; + $scope.changingCard = false; + }, + 'failure': function() { + $('#couldnotchangecardModal').modal({}); + $scope.changingCard = false; + } + }; + + PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks); + }; + + $scope.getCreditImage = function(creditInfo) { + if (!creditInfo) { return 'credit.png'; } + + var kind = creditInfo.type.toLowerCase() || 'credit'; + var supported = { + 'american express': 'amex', + 'credit': 'credit', + 'diners club': 'diners', + 'discover': 'discover', + 'jcb': 'jcb', + 'mastercard': 'mastercard', + 'visa': 'visa' + }; + + kind = supported[kind] || 'credit'; + return kind + '.png'; + }; var update = function() { if (!$scope.user && !$scope.organization) { return; } $scope.obj = $scope.user ? $scope.user : $scope.organization; $scope.invoice_email = $scope.obj.invoice_email; + + // Load the credit card information. + var url = $scope.organization ? + getRestUrl('organization', $scope.organization.name, 'card') : 'user/card'; + + var getCard = Restangular.one(url); + getCard.customGET().then(function(resp) { + $scope.currentCard = resp.card; + }); }; var save = function() { @@ -677,7 +797,8 @@ quayApp.directive('planManager', function () { scope: { 'user': '=user', 'organization': '=organization', - 'readyForPlan': '&readyForPlan' + 'readyForPlan': '&readyForPlan', + 'planChanged': '&planChanged' }, controller: function($scope, $element, PlanService, Restangular) { var hasSubscription = false; @@ -715,6 +836,10 @@ quayApp.directive('planManager', function () { PlanService.getPlan(sub.plan, function(subscribedPlan) { $scope.subscribedPlan = subscribedPlan; $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos; + + if ($scope.planChanged) { + $scope.planChanged({ 'plan': subscribedPlan }); + } if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) { $scope.limit = 'over'; diff --git a/static/js/controllers.js b/static/js/controllers.js index e4e3d5d4f..2d475f5f7 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -728,6 +728,10 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us $('.form-change-pw').popover(); + $scope.planChanged = function(plan) { + $scope.hasPaidPlan = plan && plan.price > 0; + }; + $scope.showConvertForm = function() { PlanService.getMatchingBusinessPlan(function(plan) { $scope.org.plan = plan; @@ -1206,6 +1210,10 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService $scope.membersFound = null; $scope.invoiceLoading = true; + $scope.planChanged = function(plan) { + $scope.hasPaidPlan = plan && plan.price > 0; + }; + $scope.loadInvoices = function() { if ($scope.invoices) { return; } $scope.invoiceLoading = true; diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 7df7b3fd9..46e3af4ea 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -15,7 +15,8 @@
@@ -24,15 +25,18 @@
-
+
- -
+ +
+
+ +
- Loading billing history: +
diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index db81cb629..e6c7123e6 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -27,8 +27,8 @@ @@ -38,7 +38,7 @@
-
+