Prepare for 3DS 2

This commit is contained in:
Pēteris Caune 2019-08-18 18:16:37 +03:00
parent 33dece4ad2
commit fa16bd4e42
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
9 changed files with 279 additions and 293 deletions

View File

@ -66,11 +66,9 @@ class Subscription(models.Model):
@property @property
def payment_method(self): def payment_method(self):
if not self.payment_method_token:
return None
if not hasattr(self, "_pm"): if not hasattr(self, "_pm"):
self._pm = braintree.PaymentMethod.find(self.payment_method_token) o = self._get_braintree_subscription()
self._pm = braintree.PaymentMethod.find(o.payment_method_token)
return self._pm return self._pm
def _get_braintree_subscription(self): def _get_braintree_subscription(self):
@ -79,43 +77,19 @@ class Subscription(models.Model):
return self._sub return self._sub
def get_client_token(self): def get_client_token(self):
assert self.customer_id
return braintree.ClientToken.generate({"customer_id": self.customer_id}) return braintree.ClientToken.generate({"customer_id": self.customer_id})
def update_payment_method(self, nonce): def update_payment_method(self, nonce):
# Create customer record if it does not exist: assert self.subscription_id
if not self.customer_id:
result = braintree.Customer.create({"email": self.user.email})
if not result.is_success:
return result
self.customer_id = result.customer.id result = braintree.Subscription.update(
self.save() self.subscription_id, {"payment_method_nonce": nonce}
# Create payment method
result = braintree.PaymentMethod.create(
{
"customer_id": self.customer_id,
"payment_method_nonce": nonce,
"options": {"make_default": True},
}
) )
if not result.is_success: if not result.is_success:
return result return result
self.payment_method_token = result.payment_method.token
self.save()
# Update an existing subscription to use this payment method
if self.subscription_id:
result = braintree.Subscription.update(
self.subscription_id,
{"payment_method_token": self.payment_method_token},
)
if not result.is_success:
return result
def update_address(self, post_data): def update_address(self, post_data):
# Create customer record if it does not exist: # Create customer record if it does not exist:
if not self.customer_id: if not self.customer_id:
@ -141,9 +115,9 @@ class Subscription(models.Model):
if not result.is_success: if not result.is_success:
return result return result
def setup(self, plan_id): def setup(self, plan_id, nonce):
result = braintree.Subscription.create( result = braintree.Subscription.create(
{"payment_method_token": self.payment_method_token, "plan_id": plan_id} {"payment_method_nonce": nonce, "plan_id": plan_id}
) )
if result.is_success: if result.is_success:

View File

@ -7,10 +7,14 @@ from hc.test import BaseTestCase
class GetClientTokenTestCase(BaseTestCase): class GetClientTokenTestCase(BaseTestCase):
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_it_works(self, mock_braintree): def test_it_works(self, mock_braintree):
sub = Subscription(user=self.alice)
sub.customer_id = "fake-customer-id"
sub.save()
mock_braintree.ClientToken.generate.return_value = "test-token" mock_braintree.ClientToken.generate.return_value = "test-token"
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.get("/pricing/get_client_token/") r = self.client.get("/pricing/token/")
self.assertContains(r, "test-token", status_code=200) self.assertContains(r, "test-token", status_code=200)
# A subscription object should have been created # A subscription object should have been created

View File

@ -5,23 +5,13 @@ from hc.test import BaseTestCase
class UpdatePaymentMethodTestCase(BaseTestCase): class UpdatePaymentMethodTestCase(BaseTestCase):
def _setup_mock(self, mock):
""" Set up Braintree calls that the controller will use. """
mock.PaymentMethod.create.return_value.is_success = True
mock.PaymentMethod.create.return_value.payment_method.token = "fake"
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_it_retrieves_paypal(self, mock): def test_it_retrieves_paypal(self, mock):
self._setup_mock(mock)
mock.paypal_account.PayPalAccount = dict mock.paypal_account.PayPalAccount = dict
mock.credit_card.CreditCard = list mock.credit_card.CreditCard = list
mock.PaymentMethod.find.return_value = {"email": "foo@example.org"} mock.PaymentMethod.find.return_value = {"email": "foo@example.org"}
self.sub = Subscription(user=self.alice) Subscription.objects.create(user=self.alice)
self.sub.payment_method_token = "fake-token"
self.sub.save()
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/billing/payment_method/") r = self.client.get("/accounts/profile/billing/payment_method/")
@ -29,65 +19,12 @@ class UpdatePaymentMethodTestCase(BaseTestCase):
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_it_retrieves_cc(self, mock): def test_it_retrieves_cc(self, mock):
self._setup_mock(mock)
mock.paypal_account.PayPalAccount = list mock.paypal_account.PayPalAccount = list
mock.credit_card.CreditCard = dict mock.credit_card.CreditCard = dict
mock.PaymentMethod.find.return_value = {"masked_number": "1***2"} mock.PaymentMethod.find.return_value = {"masked_number": "1***2"}
self.sub = Subscription(user=self.alice) Subscription.objects.create(user=self.alice)
self.sub.payment_method_token = "fake-token"
self.sub.save()
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/billing/payment_method/") r = self.client.get("/accounts/profile/billing/payment_method/")
self.assertContains(r, "1***2") self.assertContains(r, "1***2")
@patch("hc.payments.models.braintree")
def test_it_creates_payment_method(self, mock):
self._setup_mock(mock)
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.save()
self.client.login(username="alice@example.org", password="password")
form = {"payment_method_nonce": "test-nonce"}
r = self.client.post("/accounts/profile/billing/payment_method/", form)
self.assertRedirects(r, "/accounts/profile/billing/")
@patch("hc.payments.models.braintree")
def test_it_creates_customer(self, mock):
self._setup_mock(mock)
mock.Customer.create.return_value.is_success = True
mock.Customer.create.return_value.customer.id = "test-customer-id"
self.sub = Subscription(user=self.alice)
self.sub.save()
self.client.login(username="alice@example.org", password="password")
form = {"payment_method_nonce": "test-nonce"}
self.client.post("/accounts/profile/billing/payment_method/", form)
self.sub.refresh_from_db()
self.assertEqual(self.sub.customer_id, "test-customer-id")
@patch("hc.payments.models.braintree")
def test_it_updates_subscription(self, mock):
self._setup_mock(mock)
self.sub = Subscription(user=self.alice)
self.sub.customer_id = "test-customer"
self.sub.subscription_id = "fake-id"
self.sub.save()
mock.Customer.create.return_value.is_success = True
mock.Customer.create.return_value.customer.id = "test-customer-id"
self.client.login(username="alice@example.org", password="password")
form = {"payment_method_nonce": "test-nonce"}
self.client.post("/accounts/profile/billing/payment_method/", form)
self.assertTrue(mock.Subscription.update.called)

View File

@ -4,17 +4,17 @@ from hc.payments.models import Subscription
from hc.test import BaseTestCase from hc.test import BaseTestCase
class SetPlanTestCase(BaseTestCase): class UpdateSubscriptionTestCase(BaseTestCase):
def _setup_mock(self, mock): def _setup_mock(self, mock):
""" Set up Braintree calls that the controller will use. """ """ Set up Braintree calls that the controller will use. """
mock.Subscription.create.return_value.is_success = True mock.Subscription.create.return_value.is_success = True
mock.Subscription.create.return_value.subscription.id = "t-sub-id" mock.Subscription.create.return_value.subscription.id = "t-sub-id"
def run_set_plan(self, plan_id="P20"): def run_update(self, plan_id="P20", nonce="fake-nonce"):
form = {"plan_id": plan_id} form = {"plan_id": plan_id, "nonce": nonce}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
return self.client.post("/pricing/set_plan/", form, follow=True) return self.client.post("/pricing/update/", form, follow=True)
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_it_works(self, mock): def test_it_works(self, mock):
@ -24,8 +24,9 @@ class SetPlanTestCase(BaseTestCase):
self.profile.sms_sent = 1 self.profile.sms_sent = 1
self.profile.save() self.profile.save()
r = self.run_set_plan() r = self.run_update()
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
self.assertContains(r, "Your billing plan has been updated!")
# Subscription should be filled out: # Subscription should be filled out:
sub = Subscription.objects.get(user=self.alice) sub = Subscription.objects.get(user=self.alice)
@ -42,7 +43,10 @@ class SetPlanTestCase(BaseTestCase):
self.assertEqual(self.profile.sms_sent, 0) self.assertEqual(self.profile.sms_sent, 0)
# braintree.Subscription.cancel should have not been called # braintree.Subscription.cancel should have not been called
assert not mock.Subscription.cancel.called # because there was no previous subscription
self.assertFalse(mock.Subscription.cancel.called)
self.assertTrue(mock.Subscription.create.called)
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_yearly_works(self, mock): def test_yearly_works(self, mock):
@ -52,7 +56,7 @@ class SetPlanTestCase(BaseTestCase):
self.profile.sms_sent = 1 self.profile.sms_sent = 1
self.profile.save() self.profile.save()
r = self.run_set_plan("Y192") r = self.run_update("Y192")
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
# Subscription should be filled out: # Subscription should be filled out:
@ -80,7 +84,7 @@ class SetPlanTestCase(BaseTestCase):
self.profile.sms_sent = 1 self.profile.sms_sent = 1
self.profile.save() self.profile.save()
r = self.run_set_plan("P80") r = self.run_update("P80")
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
# Subscription should be filled out: # Subscription should be filled out:
@ -114,8 +118,9 @@ class SetPlanTestCase(BaseTestCase):
self.profile.sms_sent = 1 self.profile.sms_sent = 1
self.profile.save() self.profile.save()
r = self.run_set_plan("") r = self.run_update("")
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
self.assertContains(r, "Your billing plan has been updated!")
# Subscription should be cleared # Subscription should be cleared
sub = Subscription.objects.get(user=self.alice) sub = Subscription.objects.get(user=self.alice)
@ -130,10 +135,10 @@ class SetPlanTestCase(BaseTestCase):
self.assertEqual(self.profile.team_limit, 2) self.assertEqual(self.profile.team_limit, 2)
self.assertEqual(self.profile.sms_limit, 0) self.assertEqual(self.profile.sms_limit, 0)
assert mock.Subscription.cancel.called self.assertTrue(mock.Subscription.cancel.called)
def test_bad_plan_id(self): def test_bad_plan_id(self):
r = self.run_set_plan(plan_id="this-is-wrong") r = self.run_update(plan_id="this-is-wrong")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
@ -144,16 +149,16 @@ class SetPlanTestCase(BaseTestCase):
sub.subscription_id = "prev-sub" sub.subscription_id = "prev-sub"
sub.save() sub.save()
r = self.run_set_plan() r = self.run_update()
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
assert mock.Subscription.cancel.called self.assertTrue(mock.Subscription.cancel.called)
@patch("hc.payments.models.braintree") @patch("hc.payments.models.braintree")
def test_subscription_creation_failure(self, mock): def test_subscription_creation_failure(self, mock):
mock.Subscription.create.return_value.is_success = False mock.Subscription.create.return_value.is_success = False
mock.Subscription.create.return_value.message = "sub failure" mock.Subscription.create.return_value.message = "sub failure"
r = self.run_set_plan() r = self.run_update()
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
self.assertContains(r, "sub failure") self.assertContains(r, "sub failure")
@ -171,7 +176,7 @@ class SetPlanTestCase(BaseTestCase):
mock.Subscription.create.return_value.is_success = False mock.Subscription.create.return_value.is_success = False
mock.Subscription.create.return_value.message = "sub failure" mock.Subscription.create.return_value.message = "sub failure"
r = self.run_set_plan() r = self.run_update()
# It should cancel the current plan # It should cancel the current plan
self.assertTrue(mock.Subscription.cancel.called) self.assertTrue(mock.Subscription.cancel.called)
@ -183,3 +188,18 @@ class SetPlanTestCase(BaseTestCase):
# And it should show the error message from API: # And it should show the error message from API:
self.assertRedirects(r, "/accounts/profile/billing/") self.assertRedirects(r, "/accounts/profile/billing/")
self.assertContains(r, "sub failure") self.assertContains(r, "sub failure")
@patch("hc.payments.models.braintree")
def test_it_updates_payment_method(self, mock):
# Initial state: the user has a subscription and a high check limit:
sub = Subscription.objects.for_user(self.alice)
sub.plan_id = "P20"
sub.subscription_id = "old-sub-id"
sub.save()
r = self.run_update()
# It should update the existing subscription
self.assertTrue(mock.Subscription.update.called)
self.assertRedirects(r, "/accounts/profile/billing/")
self.assertContains(r, "Your payment method has been updated!")

View File

@ -19,9 +19,7 @@ urlpatterns = [
path( path(
"invoice/pdf/<slug:transaction_id>/", views.pdf_invoice, name="hc-invoice-pdf" "invoice/pdf/<slug:transaction_id>/", views.pdf_invoice, name="hc-invoice-pdf"
), ),
path("pricing/set_plan/", views.set_plan, name="hc-set-plan"), path("pricing/update/", views.update, name="hc-update-subscription"),
path( path("pricing/token/", views.token, name="hc-get-client-token"),
"pricing/get_client_token/", views.get_client_token, name="hc-get-client-token"
),
path("pricing/charge/", views.charge_webhook), path("pricing/charge/", views.charge_webhook),
] ]

View File

@ -20,7 +20,7 @@ from hc.payments.models import Subscription
@login_required @login_required
def get_client_token(request): def token(request):
sub = Subscription.objects.for_user(request.user) sub = Subscription.objects.for_user(request.user)
return JsonResponse({"client_token": sub.get_client_token()}) return JsonResponse({"client_token": sub.get_client_token()})
@ -96,13 +96,21 @@ def log_and_bail(request, result):
@login_required @login_required
@require_POST @require_POST
def set_plan(request): def update(request):
plan_id = request.POST["plan_id"] plan_id = request.POST["plan_id"]
nonce = request.POST["nonce"]
if plan_id not in ("", "P20", "P80", "Y192", "Y768"): if plan_id not in ("", "P20", "P80", "Y192", "Y768"):
return HttpResponseBadRequest() return HttpResponseBadRequest()
sub = Subscription.objects.for_user(request.user) sub = Subscription.objects.for_user(request.user)
if sub.plan_id == plan_id: # If plan_id has not changed then just update the payment method:
if plan_id == sub.plan_id:
error = sub.update_payment_method(nonce)
if error:
return log_and_bail(request, error)
request.session["payment_method_status"] = "success"
return redirect("hc-billing") return redirect("hc-billing")
# Cancel the previous plan and reset limits: # Cancel the previous plan and reset limits:
@ -116,9 +124,10 @@ def set_plan(request):
profile.save() profile.save()
if plan_id == "": if plan_id == "":
request.session["set_plan_status"] = "success"
return redirect("hc-billing") return redirect("hc-billing")
result = sub.setup(plan_id) result = sub.setup(plan_id, nonce)
if not result.is_success: if not result.is_success:
return log_and_bail(request, result) return log_and_bail(request, result)
@ -161,19 +170,6 @@ def address(request):
@login_required @login_required
def payment_method(request): def payment_method(request):
sub = get_object_or_404(Subscription, user=request.user) sub = get_object_or_404(Subscription, user=request.user)
if request.method == "POST":
if "payment_method_nonce" not in request.POST:
return HttpResponseBadRequest()
nonce = request.POST["payment_method_nonce"]
error = sub.update_payment_method(nonce)
if error:
return log_and_bail(request, error)
request.session["payment_method_status"] = "success"
return redirect("hc-billing")
ctx = {"sub": sub, "pm": sub.payment_method} ctx = {"sub": sub, "pm": sub.payment_method}
return render(request, "payments/payment_method.html", ctx) return render(request, "payments/payment_method.html", ctx)

View File

@ -12,7 +12,7 @@
} }
@media (min-width: 992px) { @media (min-width: 992px) {
#change-billing-plan-modal .modal-dialog { #change-billing-plan-modal .modal-dialog, #payment-method-modal .modal-dialog, #please-wait-modal .modal-dialog {
width: 850px; width: 850px;
} }
} }
@ -86,3 +86,8 @@
color: #777; color: #777;
} }
#please-wait-modal .modal-body {
text-align: center;
padding: 100px;
font-size: 18px;
}

View File

@ -1,45 +1,92 @@
$(function () { $(function () {
var clientTokenRequested = false; var preloadedToken = null;
function requestClientToken() { function getToken(callback) {
if (!clientTokenRequested) { if (preloadedToken) {
clientTokenRequested = true; callback(preloadedToken);
$.getJSON("/pricing/get_client_token/", setupDropin); } else {
$.getJSON("/pricing/token/", function(response) {
preloadedToken = response.client_token;
callback(response.client_token);
});
} }
} }
function setupDropin(data) { // Preload client token:
braintree.dropin.create({ if ($("#billing-address").length) {
authorization: data.client_token, getToken(function(token){});
container: "#dropin", }
paypal: { flow: 'vault' }
}, function(createErr, instance) { function getAmount(planId) {
$("#payment-form-submit").click(function() { return planId.substr(1);
instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) { }
$("#pmm-nonce").val(payload.nonce);
$("#payment-form").submit(); function showPaymentMethodForm(planId) {
$("#plan-id").val(planId);
$("#nonce").val("");
if (planId == "") {
// Don't need a payment method when switching to the free plan
// -- can submit the form right away:
$("#update-subscription-form").submit();
return;
}
$("#payment-form-submit").prop("disabled", true);
$("#payment-method-modal").modal("show");
getToken(function(token) {
braintree.dropin.create({
authorization: token,
container: "#dropin",
threeDSecure: {
amount: getAmount(planId),
},
paypal: { flow: 'vault' },
preselectVaultedPaymentMethod: false
}, function(createErr, instance) {
$("#payment-form-submit").off().click(function() {
instance.requestPaymentMethod(function (err, payload) {
$("#payment-method-modal").modal("hide");
$("#please-wait-modal").modal("show");
$("#nonce").val(payload.nonce);
$("#update-subscription-form").submit();
});
}); });
}).prop("disabled", false);
$("#payment-method-modal").off("hidden.bs.modal").on("hidden.bs.modal", function() {
instance.teardown();
});
instance.on("paymentMethodRequestable", function() {
$("#payment-form-submit").prop("disabled", false);
});
instance.on("noPaymentMethodRequestable", function() {
$("#payment-form-submit").prop("disabled", true);
});
});
}); });
} }
$("#update-payment-method").hover(requestClientToken); $("#change-plan-btn").click(function() {
$("#change-billing-plan-modal").modal("hide");
$("#update-payment-method").click(function() { showPaymentMethodForm(this.dataset.planId);
requestClientToken();
$("#payment-form").attr("action", this.dataset.action);
$("#payment-form-submit").text("Update Payment Method");
$("#payment-method-modal").modal("show");
}); });
$("#update-payment-method").click(function() {
showPaymentMethodForm($("#old-plan-id").val());
});
$("#billing-history").load( "/accounts/profile/billing/history/" ); $("#billing-history").load("/accounts/profile/billing/history/");
$("#billing-address").load( "/accounts/profile/billing/address/", function() { $("#billing-address").load("/accounts/profile/billing/address/", function() {
$("#billing-address input").each(function(idx, obj) { $("#billing-address input").each(function(idx, obj) {
$("#" + obj.name).val(obj.value); $("#" + obj.name).val(obj.value);
}); });
}); });
$("#payment-method").load( "/accounts/profile/billing/payment_method/", function() { $("#payment-method").load("/accounts/profile/billing/payment_method/", function() {
$("#next-billing-date").text($("#nbd").val()); $("#next-billing-date").text($("#nbd").val());
}); });
@ -94,9 +141,7 @@ $(function () {
if ($("#plan-business-plus").hasClass("selected")) { if ($("#plan-business-plus").hasClass("selected")) {
planId = period == "monthly" ? "P80" : "Y768"; planId = period == "monthly" ? "P80" : "Y768";
} }
$("#plan-id").val(planId);
if (planId == $("#old-plan-id").val()) { if (planId == $("#old-plan-id").val()) {
$("#change-plan-btn") $("#change-plan-btn")
.attr("disabled", "disabled") .attr("disabled", "disabled")
@ -105,10 +150,14 @@ $(function () {
} else { } else {
var caption = "Change Billing Plan"; var caption = "Change Billing Plan";
if (planId) { if (planId) {
caption += " And Pay $" + planId.substr(1) + " Now"; var amount = planId.substr(1);
caption += " And Pay $" + amount + " Now";
} }
$("#change-plan-btn").removeAttr("disabled").text(caption); $("#change-plan-btn")
.removeAttr("disabled")
.text(caption)
.attr("data-plan-id", planId);
} }
} }
updateChangePlanForm(); updateChangePlanForm();

View File

@ -76,16 +76,13 @@
{% endif %} {% endif %}
</div> </div>
{% if sub.subscription_id %}
<div class="panel panel-{{ payment_method_status }}"> <div class="panel panel-{{ payment_method_status }}">
<div class="panel-body settings-block"> <div class="panel-body settings-block">
<h2>Payment Method</h2> <h2>Payment Method</h2>
{% if sub.payment_method_token %}
<p id="payment-method"> <p id="payment-method">
<span class="loading">loading…</span> <span class="loading">loading…</span>
</p> </p>
{% else %}
<p id="payment-method-missing" class="billing-empty">Not set</p>
{% endif %}
<button <button
id="update-payment-method" id="update-payment-method"
class="btn btn-default pull-right"> class="btn btn-default pull-right">
@ -97,6 +94,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="panel panel-{{ address_status }}"> <div class="panel panel-{{ address_status }}">
@ -170,110 +168,104 @@
<div id="change-billing-plan-modal" class="modal"> <div id="change-billing-plan-modal" class="modal">
<div class="modal-dialog"> <div class="modal-dialog">
{% if sub.payment_method_token and sub.address_id %} {% if sub.address_id %}
<form method="post" class="form-horizontal" autocomplete="off" action="{% url 'hc-set-plan' %}"> <div class="modal-content">
{% csrf_token %} <div class="modal-header">
<input type="hidden" id="old-plan-id" value="{{ sub.plan_id }}"> <button type="button" class="close" data-dismiss="modal">&times;</button>
<input type="hidden" id="plan-id" name="plan_id" value="{{ sub.plan_id }}"> <h4>Change Billing Plan</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-4">
<div id="plan-hobbyist" class="panel plan {% if sub.plan_id == "" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<div class="modal-content"> <h2>Hobbyist</h2>
<div class="modal-header"> <ul>
<button type="button" class="close" data-dismiss="modal">&times;</button> <li>Checks: 20</li>
<h4>Change Billing Plan</h4> <li>Team members: 3</li>
</div> <li>Log entries: 100</li>
<div class="modal-body"> </ul>
<div class="row"> <h3>Free</h3>
<div class="col-sm-4">
<div id="plan-hobbyist" class="panel plan {% if sub.plan_id == "" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<h2>Hobbyist</h2>
<ul>
<li>Checks: 20</li>
<li>Team members: 3</li>
<li>Log entries: 100</li>
</ul>
<h3>Free</h3>
</div>
</div>
<div class="col-sm-4">
<div id="plan-business" class="panel plan {% if sub.plan_id == "P20" or sub.plan_id == "Y192" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<h2>Business</h2>
<ul>
<li>Checks: 100</li>
<li>Team members: 10</li>
<li>Log entries: 1000</li>
</ul>
<h3>
<span id="business-price"></span>
<small>/ month</small>
</h3>
</div>
</div>
<div class="col-sm-4">
<div id="plan-business-plus" class="panel plan {% if sub.plan_id == "P80" or sub.plan_id == "Y768" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<h2>Business Plus</h2>
<ul>
<li>Checks: 1000</li>
<li>Team members: Unlimited</li>
<li>Log entries: 1000</li>
</ul>
<h3>
<span id="business-plus-price"></span>
<small>/ month</small>
</h3>
</div>
</div> </div>
</div> </div>
<div class="col-sm-4">
<div id="plan-business" class="panel plan {% if sub.plan_id == "P20" or sub.plan_id == "Y192" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<div class="row"> <h2>Business</h2>
<div id="billing-periods" class="col-sm-6"> <ul>
<p>Billing Period</p> <li>Checks: 100</li>
<li>Team members: 10</li>
<label class="radio-container"> <li>Log entries: 1000</li>
<input </ul>
id="billing-monthly" <h3>
type="radio" <span id="business-price"></span>
name="billing_period" <small>/ month</small>
value="monthly" </h3>
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %}{% else %}checked{% endif %}>
<span class="radiomark"></span>
Monthly
</label>
<label class="radio-container">
<input
id="billing-annual"
type="radio"
name="billing_period"
value="annual"
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %} checked {% endif %}>
<span class="radiomark"></span>
Annual, 20% off
</label>
</div> </div>
</div> </div>
<div class="col-sm-4">
<div id="plan-business-plus" class="panel plan {% if sub.plan_id == "P80" or sub.plan_id == "Y768" %}selected{% endif %}">
<div class="marker">Selected Plan</div>
<div class="text-warning"> <h2>Business Plus</h2>
<strong>No proration.</strong> We currently do not <ul>
support proration when changing billing plans. <li>Checks: 1000</li>
Changing the plan starts a new billing cycle <li>Team members: Unlimited</li>
and charges your payment method. <li>Log entries: 1000</li>
</ul>
<h3>
<span id="business-plus-price"></span>
<small>/ month</small>
</h3>
</div>
</div> </div>
</div> </div>
<div class="row">
<div id="billing-periods" class="col-sm-6">
<p>Billing Period</p>
<div class="modal-footer"> <label class="radio-container">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <input
<button id="change-plan-btn" type="submit" class="btn btn-primary" disabled="disabled"> id="billing-monthly"
Change Billing Plan type="radio"
</button> name="billing_period"
value="monthly"
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %}{% else %}checked{% endif %}>
<span class="radiomark"></span>
Monthly
</label>
<label class="radio-container">
<input
id="billing-annual"
type="radio"
name="billing_period"
value="annual"
{% if sub.plan_id == "Y192" or sub.plan_id == "Y768" %} checked {% endif %}>
<span class="radiomark"></span>
Annual, 20% off
</label>
</div>
</div>
<div class="text-warning">
<strong>No proration.</strong> We currently do not
support proration when changing billing plans.
Changing the plan starts a new billing cycle
and charges your payment method.
</div> </div>
</div> </div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button id="change-plan-btn" type="button" class="btn btn-primary" disabled="disabled">
Change Billing Plan
</button>
</div>
</div>
{% else %} {% else %}
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -281,14 +273,6 @@
<h4>Some details are missing…</h4> <h4>Some details are missing…</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
{% if not sub.payment_method_token %}
<div id="no-payment-method">
<h4>No payment method.</h4>
<p>Please add a payment method before changing the billing
plan.
</p>
</div>
{% endif %}
{% if not sub.address_id %} {% if not sub.address_id %}
<div id="no-billing-address"> <div id="no-billing-address">
<h4>Country not specified.</h4> <h4>Country not specified.</h4>
@ -315,28 +299,23 @@
<div id="payment-method-modal" class="modal pm-modal"> <div id="payment-method-modal" class="modal pm-modal">
<div class="modal-dialog"> <div class="modal-dialog">
<form id="payment-form" method="post" action="{% url 'hc-payment-method' %}"> <div class="modal-content">
{% csrf_token %} <div class="modal-header">
<input id="pmm-nonce" type="hidden" name="payment_method_nonce" /> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Payment Method</h4>
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Payment Method</h4>
</div>
<div class="modal-body">
<div id="dropin"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
Confirm Payment Method
</button>
</div>
</div> </div>
</form> <div class="modal-body">
<div id="dropin"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
<button id="payment-form-submit" type="button" class="btn btn-primary" disabled>
Confirm Payment Method
</button>
</div>
</div>
</div> </div>
</div> </div>
@ -510,11 +489,35 @@
</div> </div>
</div> </div>
<div id="please-wait-modal" class="modal pm-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4>Payment Method</h4>
</div>
<div class="modal-body">
Processing, please wait&hellip;
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" disabled>
Confirm Payment Method
</button>
</div>
</div>
</div>
</div>
<form id="update-subscription-form" method="post" action="{% url 'hc-update-subscription' %}">
{% csrf_token %}
<input id="nonce" type="hidden" name="nonce" />
<input type="hidden" id="old-plan-id" value="{{ sub.plan_id }}">
<input id="plan-id" type="hidden" name="plan_id" />
</form>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://js.braintreegateway.com/web/dropin/1.17.1/js/dropin.min.js"></script> <script src="https://js.braintreegateway.com/web/dropin/1.20.0/js/dropin.min.js"></script>
{% compress js %} {% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> <script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>