1
0
mirror of https://github.com/quay/quay.git synced 2025-07-30 07:43:13 +03:00

billing: update Stripe checkout to support 3DS (PROJQUAY-5129) (#1818)

Update Stripe checkout in order to support auth requirements from
banks.
This commit is contained in:
Kenny Lee Sin Cheong
2023-04-11 14:41:37 -04:00
committed by GitHub
parent fa50c70ed0
commit 89725309be
9 changed files with 502 additions and 275 deletions

View File

@ -367,6 +367,9 @@ class FakeStripe(object):
if cus_obj._subscription and cus_obj._subscription.id == sub_id:
cus_obj._subscription = None
def default_payment_method(self, payment_method):
return "pm_somestripepaymentmethodid"
class Customer(AttrDict):
FAKE_PLAN = AttrDict(
{
@ -427,6 +430,9 @@ class FakeStripe(object):
def plan(self, plan_name):
self["new_plan"] = plan_name
def refresh(self):
return self
def save(self):
if self.get("new_card", None) is not None:
raise stripe.error.CardError(
@ -514,6 +520,26 @@ class FakeStripe(object):
}
)
class PaymentMethod(AttrDict):
FAKE_PAYMENT_METHOD = AttrDict(
{
"id": "card123",
"type": "Visa",
"card": AttrDict(
{
"last4": "4242",
"exp_month": 5,
"exp_year": 2016,
}
),
"billing_details": AttrDict({"name": "Joe User"}),
}
)
@classmethod
def retrieve(cls, payment_method_id):
return cls.FAKE_PAYMENT_METHOD
Subscription = FakeSubscription

View File

@ -20,13 +20,21 @@ from endpoints.api import (
require_scope,
abort,
)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.subscribe import subscribe, subscription_view
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
from endpoints.api.subscribe import (
get_price,
change_subscription,
subscription_view,
connection_response,
check_repository_usage,
)
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
from data.billing import PLANS, get_plan
from util.request import get_request_ip
import features
import uuid
@ -85,18 +93,15 @@ def get_card(user):
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if cus and cus.default_card:
if cus and cus.subscription:
# Find the default card.
default_card = None
for card in cus.cards.data:
if card.id == cus.default_card:
default_card = card
break
payment_method = billing.PaymentMethod.retrieve(cus.subscription.default_payment_method)
if default_card:
if payment_method.card:
default_card = payment_method.card
card_info = {
"owner": default_card.name,
"type": default_card.type,
"owner": payment_method.billing_details.name,
"type": payment_method.type,
"last4": default_card.last4,
"exp_month": default_card.exp_month,
"exp_year": default_card.exp_year,
@ -105,27 +110,6 @@ def get_card(user):
return {"card": card_info}
def set_card(user, token):
if user.stripe_id:
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if cus:
try:
cus.card = token
cus.save()
except stripe.error.CardError as exc:
return carderror_response(exc)
except stripe.error.InvalidRequestError as exc:
return carderror_response(exc)
except stripe.error.APIConnectionError as e:
return carderror_response(e)
return get_card(user)
def get_invoices(customer_id):
def invoice_view(i):
return {
@ -218,13 +202,15 @@ class UserCard(ApiResource):
"id": "UserCard",
"type": "object",
"description": "Description of a user card",
"required": [
"token",
],
"required": ["success_url", "cancel_url"],
"properties": {
"token": {
"success_url": {
"type": "string",
"description": "Stripe token that is generated by stripe checkout.js",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
@ -247,10 +233,46 @@ class UserCard(ApiResource):
Update the user's credit card.
"""
user = get_authenticated_user()
token = request.get_json()["token"]
response = set_card(user, token)
log_action("account_change_cc", user.username)
return response
assert user.stripe_id
request_data = request.get_json()
success_url = request_data["success_url"]
cancel_url = request_data["cancel_url"]
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if not cus:
raise InvalidRequest("Invalid Stripe customer")
if not cus.subscription:
raise InvalidRequest("Invalid Stripe subscription")
try:
checkout_session = stripe.checkout.Session.create(
payment_method_types=["card"],
mode="setup",
customer=user.stripe_id,
setup_intent_data={
"metadata": {
"kind": "account_change_cc",
"namespace": user.username,
"performer": user.username,
"ip": get_request_ip(),
"subscription_id": cus.subscription.id,
},
},
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as se:
abort(503, message="Cannot contact Stripe: %s" % se)
except Exception as e:
abort(500, message=str(e))
@resource("/v1/organization/<orgname>/card")
@ -267,14 +289,16 @@ class OrganizationCard(ApiResource):
"OrgCard": {
"id": "OrgCard",
"type": "object",
"description": "Description of a user card",
"required": [
"token",
],
"description": "Description of an Organization card",
"required": ["success_url", "cancel_url"],
"properties": {
"token": {
"success_url": {
"type": "string",
"description": "Stripe token that is generated by stripe checkout.js",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
@ -302,10 +326,46 @@ class OrganizationCard(ApiResource):
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.organization.get_organization(orgname)
token = request.get_json()["token"]
response = set_card(organization, token)
log_action("account_change_cc", orgname)
return response
assert organization.stripe_id
request_data = request.get_json()
success_url = request_data["success_url"]
cancel_url = request_data["cancel_url"]
try:
cus = billing.Customer.retrieve(organization.stripe_id)
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
if not cus:
raise InvalidRequest("Invalid Stripe customer")
if not cus.subscription:
raise InvalidRequest("Invalid Stripe subscription")
try:
checkout_session = stripe.checkout.Session.create(
payment_method_types=["card"],
mode="setup",
customer=organization.stripe_id,
setup_intent_data={
"metadata": {
"kind": "account_change_cc",
"namespace": organization.username,
"performer": get_authenticated_user().username,
"ip": get_request_ip(),
"subscription_id": cus.subscription.id,
},
},
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as se:
abort(503, message="Cannot contact Stripe: %s" % se)
except Exception as e:
abort(500, message=str(e))
raise Unauthorized()
@ -323,34 +383,101 @@ class UserPlan(ApiResource):
"id": "UserSubscription",
"type": "object",
"description": "Description of a user card",
"required": [
"plan",
],
"required": ["plan"],
"properties": {
"token": {
"type": "string",
"description": "Stripe token that is generated by stripe checkout.js",
},
"plan": {
"type": "string",
"description": "Plan name to which the user wants to subscribe",
},
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_user_admin()
@nickname("createUserSubscription")
@validate_json_request("UserSubscription")
def post(self):
"""
Create the user's subscription. Returns a Stripe checkout session.
"""
request_data = request.get_json()
plan = request_data["plan"]
success_url = request_data.get("success_url")
cancel_url = request_data.get("cancel_url")
if not success_url or not cancel_url:
raise InvalidRequest()
user = get_authenticated_user()
if not user.stripe_id:
try:
cus = billing.Customer.create(
email=user.email,
)
user.stripe_id = cus.id
user.save()
except stripe.error.APIConnectionError as e:
return connection_response(e)
try:
price = get_price(plan, False)
if not price:
abort(404, message="Plan not found")
checkout_session = stripe.checkout.Session.create(
line_items=[
{
"price": price["stripeId"],
"quantity": 1,
},
],
customer=user.stripe_id,
subscription_data={
"metadata": {
"kind": "account_change_plan",
"namespace": user.username,
"performer": user.username,
"ip": get_request_ip(),
"plan": price["stripeId"],
}
},
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
except Exception as e:
abort(500, message=str(e))
@require_user_admin()
@nickname("updateUserSubscription")
@validate_json_request("UserSubscription")
def put(self):
"""
Create or update the user's subscription.
Update the user's existing subscription.
"""
request_data = request.get_json()
plan = request_data["plan"]
token = request_data["token"] if "token" in request_data else None
user = get_authenticated_user()
return subscribe(user, plan, token, False) # Business features not required
if not user.stripe_id:
raise InvalidRequest()
price = get_price(plan, False)
if not price:
abort(404, message="Plan not found")
return change_subscription(user, price)
@require_user_admin()
@nickname("getUserSubscription")
@ -394,36 +521,107 @@ class OrganizationPlan(ApiResource):
"id": "OrgSubscription",
"type": "object",
"description": "Description of a user card",
"required": [
"plan",
],
"required": ["plan"],
"properties": {
"token": {
"type": "string",
"description": "Stripe token that is generated by stripe checkout.js",
},
"plan": {
"type": "string",
"description": "Plan name to which the user wants to subscribe",
},
"success_url": {
"type": "string",
"description": "Redirect url after successful checkout",
},
"cancel_url": {
"type": "string",
"description": "Redirect url after cancelled checkout",
},
},
},
}
@require_scope(scopes.ORG_ADMIN)
@nickname("updateOrgSubscription")
@nickname("createOrgSubscription")
@validate_json_request("OrgSubscription")
def put(self, orgname):
def post(self, orgname):
"""
Create or update the org's subscription.
Create the org's subscription. Returns a Stripe checkout session.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
request_data = request.get_json()
plan = request_data["plan"]
token = request_data["token"] if "token" in request_data else None
success_url = request_data.get("success_url")
cancel_url = request_data.get("cancel_url")
if not success_url or not cancel_url:
raise InvalidRequest()
organization = model.organization.get_organization(orgname)
return subscribe(organization, plan, token, True) # Business plan required
if not organization.stripe_id:
try:
cus = billing.Customer.create(
email=organization.email,
)
organization.stripe_id = cus.id
organization.save()
except stripe.error.APIConnectionError as e:
return connection_response(e)
try:
price = get_price(plan, True)
if not price:
abort(404, message="Plan not found")
checkout_session = stripe.checkout.Session.create(
line_items=[
{
"price": price["stripeId"],
"quantity": 1,
},
],
customer=organization.stripe_id,
subscription_data={
"metadata": {
"kind": "account_change_plan",
"namespace": organization.username,
"performer": get_authenticated_user().username,
"ip": get_request_ip(),
"plan": price["stripeId"],
},
},
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
)
return checkout_session
except stripe.error.APIConnectionError as e:
abort(503, message="Cannot contact Stripe")
except Exception as e:
abort(500, message=str(e))
raise Unauthorized()
@require_scope(scopes.ORG_ADMIN)
@nickname("updateOrgSubscription")
@validate_json_request("OrgSubscription")
def put(self, orgname):
"""
Update the org's subscription.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can():
request_data = request.get_json()
plan = request_data["plan"]
organization = model.organization.get_organization(orgname)
if not organization.stripe_id:
raise InvalidRequest()
price = get_price(plan, True)
if not price:
abort(404, message="Plan not found")
return change_subscription(organization, price)
raise Unauthorized()

View File

@ -52,7 +52,8 @@ def subscription_view(stripe_subscription, used_repos):
return view
def subscribe(user, plan, token, require_business_plan):
def get_price(plan, require_business_plan):
"""Billing Price (previously stripe Plan) from id."""
if not features.BILLING:
return
@ -69,6 +70,10 @@ def subscribe(user, plan, token, require_business_plan):
logger.warning("Business attempting to subscribe to personal plan: %s", user.username)
raise request_error(message="No matching plan found")
return plan_found
def change_subscription(user, plan):
private_repos = model.get_private_repo_count(user.username)
# This is the default response
@ -78,41 +83,12 @@ def subscribe(user, plan, token, require_business_plan):
}
status_code = 200
if not user.stripe_id:
# Check if a non-paying user is trying to subscribe to a free plan
if not plan_found["price"] == 0:
# They want a real paying plan, create the customer and plan
# simultaneously
card = token
try:
cus = billing.Customer.create(
email=user.email,
plan=plan,
card=card,
payment_behavior="default_incomplete",
# API versions prior to 2019-03-14 defaults to 'error_if_incomplete'
)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
log_action("account_change_plan", user.username, {"plan": plan})
except stripe.error.CardError as e:
return carderror_response(e)
except stripe.error.APIConnectionError as e:
return connection_response(e)
response_json = subscription_view(cus.subscription, private_repos)
status_code = 201
else:
# Change the plan
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.error.APIConnectionError as e:
return connection_response(e)
if plan_found["price"] == 0:
if plan["price"] == 0:
if cus.subscription is not None:
# We only have to cancel the subscription if they actually have one
try:
@ -120,14 +96,12 @@ def subscribe(user, plan, token, require_business_plan):
except stripe.error.APIConnectionError as e:
return connection_response(e)
check_repository_usage(user, plan_found)
log_action("account_change_plan", user.username, {"plan": plan})
check_repository_usage(user, plan)
log_action("account_change_plan", user.username, {"plan": plan["stripeId"]})
else:
# User may have been a previous customer who is resubscribing
modify_cus_args = {"plan": plan, "payment_behavior": "default_incomplete"}
if token:
modify_cus_args["card"] = token
modify_cus_args = {"plan": plan["stripeId"], "payment_behavior": "default_incomplete"}
try:
billing.Customer.modify(cus.id, **modify_cus_args)
@ -136,8 +110,9 @@ def subscribe(user, plan, token, require_business_plan):
except stripe.error.APIConnectionError as e:
return connection_response(e)
cus = cus.refresh()
response_json = subscription_view(cus.subscription, private_repos)
check_repository_usage(user, plan_found)
log_action("account_change_plan", user.username, {"plan": plan})
check_repository_usage(user, plan)
log_action("account_change_plan", user.username, {"plan": plan["stripeId"]})
return response_json, status_code

View File

@ -346,12 +346,27 @@ SECURITY_TESTS: List[
(UserCard, "GET", None, None, "devtable", 200),
(UserCard, "GET", None, None, "freshuser", 200),
(UserCard, "GET", None, None, "reader", 200),
(UserCard, "POST", None, {"token": "ORH4"}, None, 401),
(UserCard, "POST", None, {"success_url": "http://foo", "cancel_url": "http://bar"}, None, 401),
(UserPlan, "GET", None, None, None, 401),
(UserPlan, "GET", None, None, "devtable", 200),
(UserPlan, "GET", None, None, "freshuser", 200),
(UserPlan, "GET", None, None, "reader", 200),
(UserPlan, "PUT", None, {"plan": "1QIK"}, None, 401),
(
UserPlan,
"POST",
None,
{"plan": "1QIK", "success_url": "http://foo", "cancel_url": "http://bar"},
None,
401,
),
(
UserPlan,
"PUT",
None,
{"plan": "1QIK", "success_url": "http://foo", "cancel_url": "http://bar"},
None,
401,
),
(UserLogs, "GET", None, None, None, 401),
(UserLogs, "GET", None, None, "devtable", 200),
(UserLogs, "GET", None, None, "freshuser", 200),
@ -3976,13 +3991,42 @@ SECURITY_TESTS: List[
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, None, 401),
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, "freshuser", 403),
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, "reader", 403),
(
OrganizationCard,
"POST",
{"orgname": "buynlarge"},
{"success_url": "http://foo", "cancel_url": "http://bar"},
None,
401,
),
(
OrganizationCard,
"POST",
{"orgname": "buynlarge"},
{"success_url": "http://foo", "cancel_url": "http://bar"},
"freshuser",
403,
),
(
OrganizationCard,
"POST",
{"orgname": "buynlarge"},
{"success_url": "http://foo", "cancel_url": "http://bar"},
"reader",
403,
),
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, None, 401),
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
(
OrganizationPlan,
"POST",
{"orgname": "buynlarge"},
{"plan": "WWEI", "success_url": "http://foo", "cancel_url": "http://bar"},
None,
401,
),
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, None, 401),
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "freshuser", 403),
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "reader", 403),

View File

@ -59,7 +59,7 @@ from endpoints.api import (
page_support,
)
from endpoints.exception import NotFound, InvalidToken, InvalidRequest, DownstreamIssue
from endpoints.api.subscribe import subscribe
from endpoints.api.subscribe import change_subscription, get_price
from endpoints.common import common_login
from endpoints.csrf import generate_csrf_token, OAUTH_CSRF_TOKEN_NAME
from endpoints.decorators import (
@ -734,7 +734,8 @@ class ConvertToOrganization(ApiResource):
# Subscribe the organization to the new plan.
if features.BILLING:
plan = convert_data.get("plan", "free")
subscribe(user, plan, None, True) # Require business plans
price = get_price(plan, True)
change_subscription(user, price) # Require business plans
# Convert the user to an organization.
model.organization.convert_user_to_organization(user, admin_user)

View File

@ -113,7 +113,7 @@ def render_page_template(name, route_data=None, **kwargs):
# Add Stripe checkout if billing is enabled.
if features.BILLING:
external_scripts.append("//checkout.stripe.com/checkout.js")
external_scripts.append("//js.stripe.com/v3/")
has_contact = len(app.config.get("CONTACT_INFO", [])) > 0
contact_href = None

View File

@ -4,6 +4,7 @@ from flask import request, make_response, Blueprint
from app import billing as stripe, app
from data import model
from data.logs_model import logs_model
from data.database import RepositoryState
from auth.decorators import process_auth
from auth.permissions import ModifyRepositoryPermission
@ -30,6 +31,18 @@ webhooks = Blueprint("webhooks", __name__)
@webhooks.route("/stripe", methods=["POST"])
def stripe_webhook():
def _stripe_checkout_log_action(kind, namespace_name, performer_name, ip, metadata=None):
if not metadata:
metadata = {}
performer = data.model.users.get_user(performer_name)
logs_model.log_action(
kind,
namespace_name,
performer=performer,
ip=ip,
)
request_data = request.get_json()
logger.debug("Stripe webhook call: %s", request_data)
@ -83,6 +96,47 @@ def stripe_webhook():
if namespace:
send_payment_failed(namespace.email, namespace.username)
elif event_type == "checkout.session.completed":
mode = request_data["data"]["object"]["mode"]
if mode == "setup":
setup_intent = stripe.SetupIntent.retrieve(
request_data["data"]["object"]["setup_intent"]
)
setup_intent_metadata = setup_intent["metadata"]
payment_method = setup_intent["payment_method"]
customer = setup_intent["customer"]
subscription = setup_intent_metadata["subscription_id"]
stripe.Customer.modify(
customer,
invoice_settings={"default_payment_method": payment_method},
)
stripe.Subscription.modify(
subscription,
default_payment_method=payment_method,
)
_stripe_checkout_log_action(
setup_intent_metadata["kind"],
setup_intent_metadata["namespace"],
setup_intent_metadata["performer"],
setup_intent_metadata["ip"],
)
elif mode == "subscription":
sub = stripe.Subscription.retrieve(request_data["data"]["object"]["subscription"])
sub_metadata = sub["metadata"]
_stripe_checkout_log_action(
sub_metadata["kind"],
sub_metadata["namespace"],
sub_metadata["performer"],
sub_metadata["ip"],
metadata={"plan": sub_metadata["plan"]},
)
return make_response("Okay")

View File

@ -2,9 +2,8 @@
* Helper service for loading, changing and working with subscription plans.
*/
angular.module('quay')
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', '$location',
function(KeyService, UserService, CookieService, ApiService, Features, Config, $location) {
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', '$location', '$timeout',
function(KeyService, UserService, CookieService, ApiService, Features, Config, $location, $timeout) {
var plans = null;
var planDict = {};
var planService = {};
@ -176,25 +175,45 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
planService.createSubscription = function($scope, orgname, planId, success, failure) {
if (!Features.BILLING) { return; }
var redirectURL = $scope.redirectUrl || window.location.toString();
var subscriptionDetails = {
plan: planId,
success_url: redirectURL,
cancel_url: redirectURL
};
ApiService.createSubscription(orgname, subscriptionDetails).then(
function(resp) {
$timeout(function() {
success(resp);
document.location = resp.url;
}, 250);
},
failure
);
};
planService.updateSubscription = function($scope, orgname, planId, success, failure) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
plan: planId,
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
ApiService.updateSubscription(orgname, subscriptionDetails).then(
function(resp) {
success(resp);
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
},
failure
);
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
@ -232,15 +251,25 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
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);
var redirectURL = $scope.redirectUrl || window.location.toString();
var setCardDetails = {
success_url: redirectURL,
cancel_url: redirectURL
};
planService.createSubscription($scope, orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
planService.updateSubscription($scope, orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
@ -256,125 +285,25 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
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
var redirectURL = $scope.redirectUrl || window.location.toString();
var setCardDetails = {
success_url: redirectURL,
cancel_url: redirectURL
};
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
ApiService.setCard(orgname, setCardDetails).then(
function(resp) {
$timeout(function() {
callbacks['success'](resp)
document.location = resp.url;
}, 250);
},
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;

View File

@ -3605,8 +3605,8 @@ class TestUserCard(ApiTestCase):
def test_setusercard_error(self):
self.login(ADMIN_ACCESS_USER)
json = self.postJsonResponse(UserCard, data=dict(token="sometoken"), expected_code=402)
assert "carderror" in json
# token not a valid param anymore. This becomes a checkout session instead
json = self.postJsonResponse(UserCard, data=dict(token="sometoken"), expected_code=400)
class TestOrgCard(ApiTestCase):