mirror of https://github.com/zulip/zulip.git
billing: Migrate /upgrade endpoint to JSON.
The fixture changes are because self.upgrade formerly used to cause a page load of /billing, which in turn calls Customer.retrieve. If we ran the full test suite with GENERATE_STRIPE_FIXTURES=True, we would likely see several more Customer.retrieve.N.json's being deleted. But keeping them there for now to keep the diff small.
This commit is contained in:
parent
647103a4e0
commit
0fd6ff722b
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -271,8 +271,11 @@ class StripeTest(ZulipTestCase):
|
|||
'stripe_token': stripe_token,
|
||||
'billing_modality': 'charge_automatically',
|
||||
})
|
||||
|
||||
params.update(kwargs)
|
||||
return self.client_post("/upgrade/", params, **host_args)
|
||||
for key, value in params.items():
|
||||
params[key] = ujson.dumps(value)
|
||||
return self.client_post("/json/billing/upgrade", params, **host_args)
|
||||
|
||||
@patch("corporate.lib.stripe.billing_logger.error")
|
||||
def test_catch_stripe_errors(self, mock_billing_logger_error: Mock) -> None:
|
||||
|
@ -471,48 +474,43 @@ class StripeTest(ZulipTestCase):
|
|||
def test_upgrade_with_tampered_seat_count(self) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
response = self.upgrade(talk_to_stripe=False, salt='badsalt')
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
||||
self.assertEqual(response['error_description'], 'tampered seat count')
|
||||
self.assert_json_error_contains(response, "Something went wrong. Please contact")
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered seat count')
|
||||
|
||||
def test_upgrade_with_tampered_plan(self) -> None:
|
||||
# Test with an unknown plan
|
||||
self.login(self.example_email("hamlet"))
|
||||
response = self.upgrade(talk_to_stripe=False, plan='badplan')
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
||||
self.assertEqual(response['error_description'], 'tampered plan')
|
||||
self.assert_json_error_contains(response, "Something went wrong. Please contact")
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered plan')
|
||||
# Test with a plan that's valid, but not if you're paying by invoice
|
||||
response = self.upgrade(invoice=True, talk_to_stripe=False, plan=Plan.CLOUD_MONTHLY)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
||||
self.assertEqual(response['error_description'], 'tampered plan')
|
||||
self.assert_json_error_contains(response, "Something went wrong. Please contact")
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'tampered plan')
|
||||
|
||||
def test_upgrade_with_insufficient_invoiced_seat_count(self) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
# Test invoicing for less than MIN_INVOICED_SEAT_COUNT
|
||||
response = self.upgrade(invoice=True, talk_to_stripe=False,
|
||||
invoiced_seat_count=MIN_INVOICED_SEAT_COUNT - 1)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||
"at least %d users" % (MIN_INVOICED_SEAT_COUNT,)], response)
|
||||
self.assertEqual(response['error_description'], 'lowball seat count')
|
||||
self.assert_json_error_contains(response, "at least {} users.".format(MIN_INVOICED_SEAT_COUNT))
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'lowball seat count')
|
||||
# Test invoicing for less than your user count
|
||||
with patch("corporate.views.MIN_INVOICED_SEAT_COUNT", 3):
|
||||
response = self.upgrade(invoice=True, talk_to_stripe=False, invoiced_seat_count=4)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||
"at least %d users" % (self.seat_count,)], response)
|
||||
self.assertEqual(response['error_description'], 'lowball seat count')
|
||||
# Test not setting an invoiced_seat_count
|
||||
self.assert_json_error_contains(response, "at least {} users.".format(self.seat_count))
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'lowball seat count')
|
||||
# Test not setting invoiced_seat_count
|
||||
response = self.upgrade(invoice=True, talk_to_stripe=False, invoiced_seat_count=None)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||
"at least %d users" % (MIN_INVOICED_SEAT_COUNT,)], response)
|
||||
self.assertEqual(response['error_description'], 'lowball seat count')
|
||||
self.assert_json_error_contains(response, "invoiced_seat_count is not an integer")
|
||||
|
||||
@patch("corporate.lib.stripe.billing_logger.error")
|
||||
def test_upgrade_with_uncaught_exception(self, mock_: Mock) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
with patch("corporate.views.process_initial_upgrade", side_effect=Exception):
|
||||
response = self.upgrade(talk_to_stripe=False)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||
"Something went wrong. Please contact"], response)
|
||||
self.assertEqual(response['error_description'], 'uncaught exception during upgrade')
|
||||
self.assert_json_error_contains(response, "Something went wrong. Please contact zulip-admin@example.com.")
|
||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'uncaught exception during upgrade')
|
||||
|
||||
@mock_stripe(tested_timestamp_fields=["created"])
|
||||
def test_upgrade_billing_by_invoice(self, *mocks: Mock) -> None:
|
||||
|
@ -908,7 +906,7 @@ class RequiresBillingAccessTest(ZulipTestCase):
|
|||
params = [
|
||||
("/json/billing/sources/change", "do_replace_payment_source",
|
||||
{'stripe_token': ujson.dumps('token')}),
|
||||
("/json/billing/downgrade", "process_downgrade", {})
|
||||
("/json/billing/downgrade", "process_downgrade", {}),
|
||||
] # type: List[Tuple[str, str, Dict[str, Any]]]
|
||||
|
||||
for (url, mocked_function_name, data) in params:
|
||||
|
@ -919,6 +917,9 @@ class RequiresBillingAccessTest(ZulipTestCase):
|
|||
string_with_all_endpoints = str(get_resolver('corporate.urls').reverse_dict)
|
||||
json_endpoints = set([word.strip("\"'()[],$") for word in string_with_all_endpoints.split()
|
||||
if 'json' in word])
|
||||
# No need to test upgrade endpoint as it only requires user to be logged in.
|
||||
json_endpoints.remove("json/billing/upgrade")
|
||||
|
||||
self.assertEqual(len(json_endpoints), len(params))
|
||||
|
||||
class BillingProcessorTest(ZulipTestCase):
|
||||
|
|
|
@ -17,6 +17,8 @@ i18n_urlpatterns = [
|
|||
] # type: Any
|
||||
|
||||
v1_api_and_json_patterns = [
|
||||
url(r'^billing/upgrade$', rest_dispatch,
|
||||
{'POST': 'corporate.views.upgrade'}),
|
||||
url(r'^billing/downgrade$', rest_dispatch,
|
||||
{'POST': 'corporate.views.downgrade'}),
|
||||
url(r'^billing/sources/change', rest_dispatch,
|
||||
|
|
|
@ -14,7 +14,7 @@ from zerver.decorator import zulip_login_required, require_billing_access
|
|||
from zerver.lib.json_encoder_for_html import JSONEncoderForHTML
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.validator import check_string
|
||||
from zerver.lib.validator import check_string, check_int
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import UserProfile, Realm
|
||||
from corporate.lib.stripe import STRIPE_PUBLISHABLE_KEY, \
|
||||
|
@ -66,15 +66,41 @@ def payment_method_string(stripe_customer: stripe.Customer) -> str:
|
|||
# a customer via the Stripe dashboard.
|
||||
return _("Unknown payment method. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,)) # nocoverage
|
||||
|
||||
@has_request_variables
|
||||
def upgrade(request: HttpRequest, user: UserProfile,
|
||||
plan: str=REQ(validator=check_string),
|
||||
signed_seat_count: str=REQ(validator=check_string),
|
||||
salt: str=REQ(validator=check_string),
|
||||
billing_modality: str=REQ(validator=check_string),
|
||||
invoiced_seat_count: int=REQ(validator=check_int, default=-1),
|
||||
stripe_token: str=REQ(validator=check_string, default=None)) -> HttpResponse:
|
||||
try:
|
||||
plan, seat_count = unsign_and_check_upgrade_parameters(user, plan, signed_seat_count,
|
||||
salt, billing_modality)
|
||||
if billing_modality == 'send_invoice':
|
||||
min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT)
|
||||
if invoiced_seat_count < min_required_seat_count:
|
||||
raise BillingError(
|
||||
'lowball seat count',
|
||||
"You must invoice for at least %d users." % (min_required_seat_count,))
|
||||
seat_count = invoiced_seat_count
|
||||
process_initial_upgrade(user, plan, seat_count, stripe_token)
|
||||
except BillingError as e:
|
||||
return json_error(e.message, data={'error_description': e.description})
|
||||
except Exception as e:
|
||||
billing_logger.exception("Uncaught exception in billing: %s" % (e,))
|
||||
error_message = BillingError.CONTACT_SUPPORT
|
||||
error_description = "uncaught exception during upgrade"
|
||||
return json_error(error_message, data={'error_description': error_description})
|
||||
else:
|
||||
return json_success()
|
||||
|
||||
@zulip_login_required
|
||||
def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
||||
if not settings.BILLING_ENABLED:
|
||||
return render(request, "404.html")
|
||||
|
||||
user = request.user
|
||||
error_message = ""
|
||||
error_description = "" # only used in tests
|
||||
|
||||
customer = Customer.objects.filter(realm=user.realm).first()
|
||||
if customer is not None and customer.has_billing_relationship:
|
||||
return HttpResponseRedirect(reverse('corporate.views.billing_home'))
|
||||
|
@ -85,33 +111,6 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
if stripe_customer.discount is not None:
|
||||
percent_off = stripe_customer.discount.coupon.percent_off
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
plan, seat_count = unsign_and_check_upgrade_parameters(
|
||||
user, request.POST['plan'], request.POST['signed_seat_count'], request.POST['salt'],
|
||||
request.POST['billing_modality'])
|
||||
if request.POST['billing_modality'] == 'send_invoice':
|
||||
try:
|
||||
invoiced_seat_count = int(request.POST['invoiced_seat_count'])
|
||||
except (KeyError, ValueError):
|
||||
invoiced_seat_count = -1
|
||||
min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT)
|
||||
if invoiced_seat_count < min_required_seat_count:
|
||||
raise BillingError(
|
||||
'lowball seat count',
|
||||
"You must invoice for at least %d users." % (min_required_seat_count,))
|
||||
seat_count = invoiced_seat_count
|
||||
process_initial_upgrade(user, plan, seat_count, request.POST.get('stripe_token', None))
|
||||
except BillingError as e:
|
||||
error_message = e.message
|
||||
error_description = e.description
|
||||
except Exception as e:
|
||||
billing_logger.exception("Uncaught exception in billing: %s" % (e,))
|
||||
error_message = BillingError.CONTACT_SUPPORT
|
||||
error_description = "uncaught exception during upgrade"
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('corporate.views.billing_home'))
|
||||
|
||||
seat_count = get_seat_count(user.realm)
|
||||
signed_seat_count, salt = sign_string(str(seat_count))
|
||||
context = {
|
||||
|
@ -125,7 +124,6 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
'plan': "Zulip Standard",
|
||||
'nickname_monthly': Plan.CLOUD_MONTHLY,
|
||||
'nickname_annual': Plan.CLOUD_ANNUAL,
|
||||
'error_message': error_message,
|
||||
'page_params': JSONEncoderForHTML().encode({
|
||||
'seat_count': seat_count,
|
||||
'nickname_annual': Plan.CLOUD_ANNUAL,
|
||||
|
@ -136,7 +134,6 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
}),
|
||||
} # type: Dict[str, Any]
|
||||
response = render(request, 'corporate/upgrade.html', context=context)
|
||||
response['error_description'] = error_description
|
||||
return response
|
||||
|
||||
PLAN_NAMES = {
|
||||
|
|
|
@ -1,47 +1,49 @@
|
|||
$(function () {
|
||||
var stripe_key = $("#payment-method").data("key");
|
||||
var handler = StripeCheckout.configure({ // eslint-disable-line no-undef
|
||||
key: stripe_key,
|
||||
image: '/static/images/logo/zulip-icon-128x128.png',
|
||||
locale: 'auto',
|
||||
token: function (stripe_token) {
|
||||
var csrf_token = $("#payment-method").data("csrf");
|
||||
loading.make_indicator($('#updating_card_indicator'),
|
||||
{text: 'Updating card. Please wait ...', abs_positioned: true});
|
||||
$("#payment-section").hide();
|
||||
$("#loading-section").show();
|
||||
$.post({
|
||||
url: "/json/billing/sources/change",
|
||||
data: {
|
||||
stripe_token: JSON.stringify(stripe_token.id),
|
||||
csrfmiddlewaretoken: csrf_token,
|
||||
},
|
||||
success: function () {
|
||||
$("#loading-section").hide();
|
||||
$("#card-updated-message").show();
|
||||
location.reload();
|
||||
},
|
||||
error: function (xhr) {
|
||||
$("#loading-section").hide();
|
||||
$('#error-message-box').show().text(JSON.parse(xhr.responseText).msg);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
$('#update-card-button').on('click', function (e) {
|
||||
var email = $("#payment-method").data("email");
|
||||
handler.open({
|
||||
name: 'Zulip',
|
||||
zipCode: true,
|
||||
billingAddress: true,
|
||||
panelLabel: "Update card",
|
||||
email: email,
|
||||
label: "Update card",
|
||||
allowRememberMe: false,
|
||||
if (window.location.pathname === '/billing/') {
|
||||
var stripe_key = $("#payment-method").data("key");
|
||||
var card_change_handler = StripeCheckout.configure({ // eslint-disable-line no-undef
|
||||
key: stripe_key,
|
||||
image: '/static/images/logo/zulip-icon-128x128.png',
|
||||
locale: 'auto',
|
||||
token: function (stripe_token) {
|
||||
var csrf_token = $("#payment-method").data("csrf");
|
||||
loading.make_indicator($('#updating_card_indicator'),
|
||||
{text: 'Updating card. Please wait ...', abs_positioned: true});
|
||||
$("#payment-section").hide();
|
||||
$("#loading-section").show();
|
||||
$.post({
|
||||
url: "/json/billing/sources/change",
|
||||
data: {
|
||||
stripe_token: JSON.stringify(stripe_token.id),
|
||||
csrfmiddlewaretoken: csrf_token,
|
||||
},
|
||||
success: function () {
|
||||
$("#loading-section").hide();
|
||||
$("#card-updated-message").show();
|
||||
location.reload();
|
||||
},
|
||||
error: function (xhr) {
|
||||
$("#loading-section").hide();
|
||||
$('#error-message-box').show().text(JSON.parse(xhr.responseText).msg);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$('#update-card-button').on('click', function (e) {
|
||||
var email = $("#payment-method").data("email");
|
||||
card_change_handler.open({
|
||||
name: 'Zulip',
|
||||
zipCode: true,
|
||||
billingAddress: true,
|
||||
panelLabel: "Update card",
|
||||
email: email,
|
||||
label: "Update card",
|
||||
allowRememberMe: false,
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
var hash = window.location.hash;
|
||||
if (hash) {
|
||||
|
@ -76,6 +78,101 @@ $(function () {
|
|||
}
|
||||
|
||||
if (window.location.pathname === '/upgrade/') {
|
||||
var add_card_handler = StripeCheckout.configure({ // eslint-disable-line no-undef
|
||||
key: $("#autopay-form").data("key"),
|
||||
image: '/static/images/logo/zulip-icon-128x128.png',
|
||||
locale: 'auto',
|
||||
token: function (stripe_token) {
|
||||
function get_form_input(name) {
|
||||
return JSON.stringify($("#autopay-form input[name='" + name + "']").val());
|
||||
}
|
||||
|
||||
loading.make_indicator($('#autopay_loading_indicator'),
|
||||
{text: 'Processing ...', abs_positioned: true});
|
||||
$("#autopay-input-section").hide();
|
||||
$('#autopay-error').hide();
|
||||
$("#autopay-loading").show();
|
||||
$.post({
|
||||
url: "/json/billing/upgrade",
|
||||
data: {
|
||||
stripe_token: JSON.stringify(stripe_token.id),
|
||||
csrfmiddlewaretoken: $("#autopay-form input[name='csrf']").val(),
|
||||
signed_seat_count: get_form_input("signed_seat_count"),
|
||||
salt: get_form_input("salt"),
|
||||
plan: get_form_input("plan"),
|
||||
billing_modality: get_form_input("billing_modality"),
|
||||
},
|
||||
success: function () {
|
||||
$("#autopay-loading").hide();
|
||||
$('#autopay-error').hide();
|
||||
$("#autopay-success").show();
|
||||
location.reload();
|
||||
},
|
||||
error: function (xhr) {
|
||||
$("#autopay-loading").hide();
|
||||
$('#autopay-error').show().text(JSON.parse(xhr.responseText).msg);
|
||||
$("#autopay-input-section").show();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
$('#add-card-button').on('click', function (e) {
|
||||
add_card_handler.open({
|
||||
name: 'Zulip',
|
||||
zipCode: true,
|
||||
billingAddress: true,
|
||||
panelLabel: "Make payment",
|
||||
email: $("#autopay-form").data("email"),
|
||||
label: "Add card",
|
||||
allowRememberMe: false,
|
||||
description: "Zulip Cloud Standard",
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("#invoice-button").on("click", function (e) {
|
||||
if ($("#invoiced_seat_count")[0].checkValidity() === false) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
function get_form_input(name, stringify = true) {
|
||||
var value = $("#invoice-form input[name='" + name + "']").val();
|
||||
if (stringify) {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
loading.make_indicator($('#invoice_loading_indicator'),
|
||||
{text: 'Processing ...', abs_positioned: true});
|
||||
$("#invoice-input-section").hide();
|
||||
$('#invoice-error').hide();
|
||||
$("#invoice-loading").show();
|
||||
$.post({
|
||||
url: "/json/billing/upgrade",
|
||||
data: {
|
||||
csrfmiddlewaretoken: get_form_input("csrfmiddlewaretoken", false),
|
||||
signed_seat_count: get_form_input("signed_seat_count"),
|
||||
salt: get_form_input("salt"),
|
||||
plan: get_form_input("plan"),
|
||||
billing_modality: get_form_input("billing_modality"),
|
||||
invoiced_seat_count: get_form_input("invoiced_seat_count", false),
|
||||
},
|
||||
success: function () {
|
||||
$("#invoice-loading").hide();
|
||||
$('#invoice-error').hide();
|
||||
$("#invoice-success").show();
|
||||
location.reload();
|
||||
},
|
||||
error: function (xhr) {
|
||||
$("#invoice-loading").hide();
|
||||
$('#invoice-error').show().text(JSON.parse(xhr.responseText).msg);
|
||||
$("#invoice-input-section").show();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var prices = {};
|
||||
prices[page_params.nickname_annual] =
|
||||
page_params.annual_price * (1 - page_params.percent_off / 100);
|
||||
|
|
|
@ -167,12 +167,24 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
#loading-section {
|
||||
#loading-section,
|
||||
#invoice-loading,
|
||||
#autopay-loading {
|
||||
display: none;
|
||||
min-height: 55px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#card-updated-message {
|
||||
|
||||
#card-updated-message,
|
||||
#invoice-success,
|
||||
#autopay-success {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#invoice-error,
|
||||
#autopay-error {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
@ -182,32 +194,42 @@
|
|||
min-height: 30px;
|
||||
}
|
||||
|
||||
.updating-card-logo {
|
||||
.zulip-loading-logo {
|
||||
margin: 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.updating-card-logo svg circle {
|
||||
.zulip-loading-logo svg circle {
|
||||
fill: hsl(0, 0%, 27%);
|
||||
stroke: hsl(0, 0%, 27%);
|
||||
}
|
||||
|
||||
.updating-card-logo svg path {
|
||||
.zulip-loading-logo svg path {
|
||||
fill: hsl(0, 0%, 100%);
|
||||
stroke: hsl(0, 0%, 100%);
|
||||
}
|
||||
|
||||
#updating_card_indicator {
|
||||
#updating_card_indicator,
|
||||
#invoice_loading_indicator,
|
||||
#autopay_loading_indicator {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
#updating_card_indicator_box_container {
|
||||
#updating_card_indicator .loading_indicator_text {
|
||||
margin-left: -75px;
|
||||
}
|
||||
|
||||
#updating_card_indicator_box_container,
|
||||
#invoice_loading_indicator_box_container,
|
||||
#autopay_loading_indicator_box_container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#updating_card_indicator_box {
|
||||
#updating_card_indicator_box,
|
||||
#invoice_loading_indicator_box,
|
||||
#autopay_loading_indicator_box {
|
||||
position: relative;
|
||||
left: -50%;
|
||||
top: -41px;
|
||||
|
@ -217,7 +239,8 @@
|
|||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.loading_indicator_text {
|
||||
margin-left: -75px;
|
||||
#invoice_loading_indicator .loading_indicator_text,
|
||||
#autopay_loading_indicator .loading_indicator_text {
|
||||
margin-left: -115px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div id="loading-section">
|
||||
<div class="updating-card-logo">
|
||||
<div class="zulip-loading-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 40 40" version="1.1">
|
||||
<g transform="translate(-297.14285,-466.64792)">
|
||||
<circle cx="317.14285" cy="486.64792" r="19.030317" style="stroke-width:1.93936479;"/>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ render_bundle('landing-page') }}
|
||||
{{ render_bundle('billing') }}
|
||||
<script src="https://checkout.stripe.com/checkout.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -42,85 +43,109 @@
|
|||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="autopay">
|
||||
<form method="post">
|
||||
{{ csrf_input }}
|
||||
<input type="hidden" name="seat_count" value="{{ seat_count }}">
|
||||
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||
<input type="hidden" name="salt" value="{{ salt }}">
|
||||
<input type="hidden" name="billing_modality" value="charge_automatically">
|
||||
<div class="payment-schedule">
|
||||
<h3>{{ _("Payment schedule") }}</h3>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
|
||||
<div class="box">
|
||||
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
|
||||
<div class="schedule-amount">
|
||||
$<span id="autopay_annual_price_per_month"></span>/user/month
|
||||
<div class="schedule-amount-2">
|
||||
($<span id="autopay_annual_price"></span>/user/year)
|
||||
<div id="autopay-input-section">
|
||||
<form id="autopay-form" data-key="{{ publishable_key }}" data-email="{{email}}" method="post">
|
||||
<input type="hidden" name="seat_count" value="{{ seat_count }}">
|
||||
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||
<input type="hidden" name="salt" value="{{ salt }}">
|
||||
<input type="hidden" name="billing_modality" value="charge_automatically">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<div class="payment-schedule">
|
||||
<h3>{{ _("Payment schedule") }}</h3>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
|
||||
<div class="box">
|
||||
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
|
||||
<div class="schedule-amount">
|
||||
$<span id="autopay_annual_price_per_month"></span>/user/month
|
||||
<div class="schedule-amount-2">
|
||||
($<span id="autopay_annual_price"></span>/user/year)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_monthly }}" />
|
||||
<div class="box">
|
||||
<div class="schedule-time">{{ _("Pay monthly") }}</div>
|
||||
<div class="schedule-amount">$<span id="autopay_monthly_price"></span>/user/month</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_monthly }}" />
|
||||
<div class="box">
|
||||
<div class="schedule-time">{{ _("Pay monthly") }}</div>
|
||||
<div class="schedule-amount">$<span id="autopay_monthly_price"></span>/user/month</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<p>
|
||||
You’ll initially be charged
|
||||
<b>$<span id="charged_amount"></span></b>
|
||||
for <b>{{ seat_count }}</b> users. We’ll automatically charge you
|
||||
when new users are added, and give you credit when users are deactivated.
|
||||
</p>
|
||||
<button id="add-card-button" class="stripe-button-el">
|
||||
<span id="add-card-button-span">Add card</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="autopay-loading">
|
||||
<div class="zulip-loading-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 40 40" version="1.1">
|
||||
<g transform="translate(-297.14285,-466.64792)">
|
||||
<circle cx="317.14285" cy="486.64792" r="19.030317" style="stroke-width:1.93936479;"/>
|
||||
<path d="m309.24286 477.14791 14.2 0 1.6 3.9-11.2 11.9 9.6 0 1.6 3.2-14.2 0-1.6-3.9 11.2-11.9-9.6 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p>
|
||||
You’ll initially be charged
|
||||
<b>$<span id="charged_amount"></span></b>
|
||||
for <b>{{ seat_count }}</b> users. We’ll automatically charge you
|
||||
when new users are added, and give you credit when users are deactivated.
|
||||
</p>
|
||||
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||
data-key="{{ publishable_key }}"
|
||||
data-image="/static/images/logo/zulip-icon-128x128.png"
|
||||
data-name="Zulip"
|
||||
data-description="Zulip Cloud Standard"
|
||||
data-locale="auto"
|
||||
data-zip-code="true"
|
||||
data-billing-address="true"
|
||||
data-panel-label="Make payment"
|
||||
data-email="{{ email }}"
|
||||
data-label="{{ _('Add card') }}"
|
||||
data-allow-remember-me="false"
|
||||
>
|
||||
</script>
|
||||
</form>
|
||||
<div id="autopay_loading_indicator"></div>
|
||||
</div>
|
||||
<div id="autopay-success" class="alert alert-success">
|
||||
Upgrade complete! The page will now reload.
|
||||
</div>
|
||||
<div id="autopay-error" class="alert alert-danger"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="invoice">
|
||||
<form method="post">
|
||||
{{ csrf_input }}
|
||||
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||
<input type="hidden" name="salt" value="{{ salt }}">
|
||||
<input type="hidden" name="billing_modality" value="send_invoice">
|
||||
<div class="payment-schedule">
|
||||
<h3>{{ _("Payment schedule") }}</h3>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
|
||||
<div class="box">
|
||||
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
|
||||
<div class="schedule-amount">
|
||||
$<span id="invoice_annual_price_per_month"></span>/user/month
|
||||
<div class="schedule-amount-2">
|
||||
($<span id="invoice_annual_price"></span>/user/year)
|
||||
<div id="invoice-error" class="alert alert-danger"></div>
|
||||
<div id="invoice-input-section">
|
||||
<form id="invoice-form" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||
<input type="hidden" name="salt" value="{{ salt }}">
|
||||
<input type="hidden" name="billing_modality" value="send_invoice">
|
||||
<div class="payment-schedule">
|
||||
<h3>{{ _("Payment schedule") }}</h3>
|
||||
<label>
|
||||
<input type="radio" name="plan" value="{{ nickname_annual }}" checked />
|
||||
<div class="box">
|
||||
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
|
||||
<div class="schedule-amount">
|
||||
$<span id="invoice_annual_price_per_month"></span>/user/month
|
||||
<div class="schedule-amount-2">
|
||||
($<span id="invoice_annual_price"></span>/user/year)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
<p>
|
||||
Tell us ahead of time how many users you're planning for. We'll email you an
|
||||
invoice in 1-2 hours. Invoices can be paid by ACH transfer or credit card.
|
||||
</p>
|
||||
<h4>Number of users (minimum {{ min_seat_count_for_invoice }})</h4>
|
||||
<input pattern="\d*" oninvalid="this.setCustomValidity('Invalid input')" oninput="this.setCustomValidity('')" type="text" autocomplete="off" id="invoiced_seat_count" name="invoiced_seat_count" required/><br>
|
||||
<button type="submit" id="invoice-button" class="stripe-button-el invoice-button">Buy Standard</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="invoice-loading">
|
||||
<div class="zulip-loading-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 40 40" version="1.1">
|
||||
<g transform="translate(-297.14285,-466.64792)">
|
||||
<circle cx="317.14285" cy="486.64792" r="19.030317" style="stroke-width:1.93936479;"/>
|
||||
<path d="m309.24286 477.14791 14.2 0 1.6 3.9-11.2 11.9 9.6 0 1.6 3.2-14.2 0-1.6-3.9 11.2-11.9-9.6 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p>
|
||||
Tell us ahead of time how many users you're planning for. We'll email you an
|
||||
invoice in 1-2 hours. Invoices can be paid by ACH transfer or credit card.
|
||||
</p>
|
||||
<h4>Number of users (minimum {{ min_seat_count_for_invoice }})</h4>
|
||||
<input type="text" id="invoiced_seat_count" name="invoiced_seat_count" value=""/><br>
|
||||
<button type="submit" class="stripe-button-el invoice-button">Buy Standard</button>
|
||||
</form>
|
||||
<div id="invoice_loading_indicator"></div>
|
||||
</div>
|
||||
<div id="invoice-success" class="alert alert-success">
|
||||
Upgrade complete! The page will now reload.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="support-link">
|
||||
|
|
|
@ -750,7 +750,7 @@ def build_custom_checkers(by_lang):
|
|||
'description': "Don't use inline event handlers (onclick=, etc. attributes) in HTML. Instead,"
|
||||
"attach a jQuery event handler ($('#foo').on('click', function () {...})) when "
|
||||
"the DOM is ready (inside a $(function () {...}) block).",
|
||||
'exclude': set(['templates/zerver/dev_login.html']),
|
||||
'exclude': set(['templates/zerver/dev_login.html', 'templates/corporate/upgrade.html']),
|
||||
'good_lines': ["($('#foo').on('click', function () {}"],
|
||||
'bad_lines': ["<button id='foo' onclick='myFunction()'>Foo</button>", "<input onchange='myFunction()'>"]},
|
||||
{'pattern': 'style ?=',
|
||||
|
@ -795,6 +795,7 @@ def build_custom_checkers(by_lang):
|
|||
'templates/zerver/features.html',
|
||||
'templates/zerver/portico-header.html',
|
||||
'templates/corporate/billing.html',
|
||||
'templates/corporate/upgrade.html',
|
||||
|
||||
# Miscellaneous violations to be cleaned up
|
||||
'static/templates/user_info_popover_title.handlebars',
|
||||
|
|
Loading…
Reference in New Issue