Skip to content

Instantly share code, notes, and snippets.

@czue
Created December 17, 2020 06:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save czue/fcda8a328a5d82dc0c582518341be59c to your computer and use it in GitHub Desktop.
Save czue/fcda8a328a5d82dc0c582518341be59c to your computer and use it in GitHub Desktop.
subscriptions
{% extends "portal_base.html" %}
{% load staticfiles %}
{% block title %} Payment {% endblock %}
{% block content %}
<div class="dashboard-interactions">
<div class="interactions-wrap full-width">
<div class="interactions-panel">
<h1 class="interactions-panel-title">Select Plan Type</h1>
<div class="interactions-panel-main">
<div class="plan-list">
<div class="row justify-content-center">
<div class="col-xl-4 col-lg-6">
<div class="plan-list-item disabled">
<h2>Business</h2>
<div class="price"><strong>$100</strong>/month</div>
<p class="info">Built for a small business or team doing up to 1 event per week.</p>
<ul>
<li><strong>50</strong> Active Events</li>
<li>Pulse Community</li>
<li>Company Page</li>
<li>Dashboards</li>
<li>Team & Client Management</li>
<li>Event Analytics</li>
<li>Event Tools</li>
<li><strong>9-5</strong> Support</li>
<li>-</li>
<li>-</li>
</ul>
<a href="#0" class="btn">Select Plan</a>
</div>
</div>
<div class="col-xl-4 col-lg-6">
<div class="plan-list-item purple">
<h2>Enterprise</h2>
<div class="price"><strong>$265</strong>/month</div>
<p class="info">Built for scaling businesses and teams doing MORE than 1 event per week.</p>
<ul>
<li><strong>500</strong> Active Events</li>
<li>Pulse Community</li>
<li>Company Page</li>
<li>Dashboards</li>
<li>Team & Client Management</li>
<li>Event Analytics</li>
<li>Event Tools</li>
<li><strong>24/7</strong> Support</li>
<li><strong>1 on 1 Training</strong></li>
<li><strong>Custom Features</strong></li>
</ul>
<a href="#0" class="btn">Select Plan</a>
</div>
</div>
<div class="col-xl-4 col-lg-6 d-block">
<div class="plan-list-item blue disabled">
<h2>Custom</h2>
<div class="text-area">
<p>Contact us for greater than 500 active events or license agreements.</p>
</div>
<a href="#0" class="btn">Contact us</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- dashboard-interactions -->
<div class="interactions-panel interactions-panel-form">
<h1 class="interactions-panel-title">Payment</h1>
<div class="interactions-panel-main">
<div class="form-group-holder">
<div class="text-holder">
<h2>Credit Card Details</h2>
</div>
<form>
<div class="form-controls-group">
<input type="text" class="form-control" placeholder="Card Number">
<input type="text" class="form-control" placeholder="MM / YY">
<input type="text" class="form-control" placeholder="CVC">
</div>
</form>
</div>
<button type="button" class="btn btn-primary btn-rounded">Subscribe</button>
</div>
</div>
</div> <!-- /interactions-wrap -->
</div><!-- /dashboard-interactions -->
<!-- from Saas Pegasus -->
<section class="section app-card">
<div class="columns columns-reversed">
<div class="column is-three-quarters">
<div class="level">
<div class="level-left">
<h1 class="title is-size-4">My Subscription</h1>
</div>
</div>
<p class="subtitle">You're not currently subscribed to a plan.</p>
<p class="my-1">
Choose the plan that best fits your needs below to gain access
to additional functionality on our site!
</p>
</div>
</div>
<hr>
<!-- dynamic monthly vs yearly -->
{% if active_plan_intervals|length > 1 %}
<div class="buttons has-addons is-centered">
{% for interval in active_plan_intervals %}
<button class="button" id="plan-selector-{{interval.interval}}">{{ interval.name }}</button>
{% endfor %}
</div>
<div class="help is-size-6 has-text-centered" id="plan-help" ></div>
{% endif %}
<!-- active products in stripe -->
<div class="columns my-2" id="plan-selector">
{% for product in active_products %}
<div class="column">
<div class="plan{% if product.metadata.is_default %} is-selected{% endif %}"
data-product-id="{{ product.stripe_id }}" data-plan-id="{{ product.default_plan.id }}" >
<div class="plan-summary">
<div class="plan-icon">
<span class="icon is-medium">
<i class="fa"></i>
</span>
</div>
<div class="plan-details">
<p class="title is-size-4">{{ product.metadata.name }}</p>
</div>
</div>
<p class="plan-tagline is-size-6">{{ product.metadata.description }}</p>
<div class="plan-price has-text-centered my-2">
<p><span class="price"></span><span class="interval"></span></p>
</div>
<ul id="upgrade-features">
{% for feature in product.metadata.features %}
<li>
<span class="icon"><i class="fa fa-check"></i></span>
<span class="upgrade-feature">{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endfor %}
</div>
<div class="columns">
<div class="column is-three-quarters">
<form id="subscription-form">
{% include 'stripe/components/card_element.html' %}
<button type="submit" id="subscribe-button" class="button is-primary">Upgrade</button>
<div class="my-1" id="payment-details" ></div>
</form>
</div>
</div>
</section>
{% endblock %}
{% block component_js %}
{{ active_products_json|json_script:'active-products' }}
{{ payment_metadata|json_script:'payment-metadata' }}
<script src="https://js.stripe.com/v3/"></script>
<script src="{% static 'js/app-bundle.js' %}"></script>
<script>
// grab our JS Payments module for convenience
const App = SiteJS.app;
const Payments = App.Payments;
const activeProducts = JSON.parse(document.getElementById('active-products').textContent);
const paymentMetadata = JSON.parse(document.getElementById('payment-metadata').textContent);
const userEmail = '{{ user.email }}';
const createCustomerUrl = "{{ subscription_urls.create_customer }}";
const subscriptionSuccessUrl = "{{ subscription_urls.subscription_success }}";
const defaultAnnual = '{{ default_to_annual }}' === 'True';
const stripe = Stripe('{{ stripe_api_key }}');
const cardElement = Payments.createCardElement(stripe);
const form = document.getElementById('subscription-form');
const subscribeButton = document.getElementById('subscribe-button');
// prevents submissions if one is already in progress
let submissionPending = false;
const getSelectedPlanElement = function () {
return document.querySelector('.plan.is-selected');
};
const handleError = function (errorMessage) {
Payments.showOrClearError(errorMessage);
subscribeButton.classList.remove('is-loading');
};
const handleSubscriptionSuccess = function () {
submissionPending = false;
location.href = subscriptionSuccessUrl;
};
const handlePaymentMethodCreated = function (result) {
if (result.error) {
handleError(result.error.message);
submissionPending = false;
} else {
let selectedPlanElement = getSelectedPlanElement();
let selectedPlan = selectedPlanElement.dataset.planId;
const paymentParams = {...paymentMetadata};
paymentParams.plan_id = selectedPlan;
paymentParams.payment_method = result.paymentMethod.id;
fetch(createCustomerUrl, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': App.Cookies.get('csrftoken'),
},
credentials: 'same-origin',
body: JSON.stringify(paymentParams),
}).then(function(response) {
return response.json();
}).then(function(result) {
if (result.error) {
handleError(result.error.message);
submissionPending = false;
} else {
const subscription = result.subscription;
// check/handle error cases https://stripe.com/docs/billing/subscriptions/set-up-subscription#manage-sub-status
const { latest_invoice } = subscription;
const { payment_intent } = latest_invoice;
if (payment_intent) {
const { client_secret, status } = payment_intent;
if (status === 'requires_action') {
// trigger 3D-secure workflow
stripe.confirmCardPayment(client_secret).then(function(result) {
if (result.error) {
// The card was declined (i.e. insufficient funds, card has expired, etc)
handleError(result.error.message);
submissionPending = false;
} else {
// Show a success message to your customer
handleSubscriptionSuccess();
}
});
} else {
// No additional information was needed
// Show a success message to your customer
handleSubscriptionSuccess();
}
}
}
}).catch(function (error) {
handleError("Sorry, there was an unexpected error processing your payment. Please contact us for support.");
submissionPending = false;
});
}
};
// from: https://stripe.com/docs/billing/subscriptions/set-up-subscription
form.addEventListener('submit', function(event) {
// We don't want to let default form submission happen here,
// which would refresh the page.
event.preventDefault();
if (getSelectedPlanElement() && !submissionPending) {
submissionPending = true;
subscribeButton.classList.add('is-loading');
stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
email: userEmail,
},
}).then(handlePaymentMethodCreated);
}
});
// hook up "monthly"/"annual" selection events
const monthlySelector = document.getElementById('plan-selector-month');
const annualSelector = document.getElementById('plan-selector-year');
const helpLabel = document.getElementById('plan-help');
const planElements = document.getElementsByClassName('plan');
const paymentDetailsElement = document.getElementById('payment-details');
const annualHelpText = "You're getting two months free by choosing an Annual plan!";
const monthlyHelpText = "Upgrade to annual pricing to get two free months.";
const updatePlans = function (isAnnual) {
for (let i = 0; i < planElements.length; i++) {
let planElt = planElements[i];
let productId = planElt.dataset.productId;
let planMetadata = (
isAnnual ? activeProducts[productId]['annual_plan'] : activeProducts[productId]['monthly_plan']
);
// set data attribute
planElt.dataset.planId = planMetadata.stripe_id;
planElt.dataset.interval = isAnnual ? 'year' : 'month';
planElt.dataset.paymentAmount = planMetadata.payment_amount;
let priceElt = planElt.querySelector('.price');
priceElt.textContent = planMetadata.monthly_amount;
let intervalElt = planElt.querySelector('.interval');
intervalElt.textContent = '/ month'; // todo: support annual display pricing
}
};
const updatePaymentDetails = function () {
let selectedPlan = getSelectedPlanElement();
if (selectedPlan) {
paymentDetailsElement.innerText = "Your card will be charged " + selectedPlan.dataset.paymentAmount +
" for your first " + selectedPlan.dataset.interval + ".";
} else {
paymentDetailsElement.innerText = "Select a plan to continue.";
}
};
const selectPeriod = function (isAnnual) {
if (isAnnual) {
if (annualSelector) {
annualSelector.classList.add('is-selected', 'is-primary');
monthlySelector.classList.remove('is-selected', 'is-primary');
helpLabel.innerText = annualHelpText;
helpLabel.classList.add('is-primary');
helpLabel.classList.remove('is-danger');
}
updatePlans(isAnnual);
} else {
if (monthlySelector) {
annualSelector.classList.remove('is-selected', 'is-primary');
monthlySelector.classList.add('is-selected', 'is-primary');
helpLabel.innerText = monthlyHelpText;
helpLabel.classList.add('is-danger');
helpLabel.classList.remove('is-primary');
}
updatePlans(isAnnual);
}
updatePaymentDetails();
};
selectPeriod(defaultAnnual);
if (annualSelector) {
annualSelector.addEventListener('click', function (event) {
selectPeriod(true);
});
}
if (monthlySelector) {
monthlySelector.addEventListener('click', function (event) {
selectPeriod(false);
});
}
// hook up plan selection events
const selectPlan = function(plan) {
// if already selected there's nothing to do
if (!plan.classList.contains('is-selected')) {
plan.classList.add('is-selected');
for (let i = 0; i < planElements.length; i++) {
if (planElements[i] !== plan) {
planElements[i].classList.remove('is-selected');
}
}
}
updatePaymentDetails();
};
for (let i = 0; i < planElements.length; i++) {
planElements[i].addEventListener('click', function(event) {
let plan = event.target.closest('.plan');
selectPlan(plan);
});
}
</script>
{% endblock component_js %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment