analytics/lib/plausible_web/templates/billing/change_plan.html.eex
RobertJoonas 8bc86d165f
Use Phoenix LiveView for the upgrade page (#3382)
* add a new upgrade page liveview behind a FF

* Create plans_v4.json file

* Add the upgrade page UI template and some basic functionalities

* different content based on subscription plan existing or not
* pageview slider
* monthly/yearly switch

* fix tests

* split into 2 separate functions

* rename variables

* implement volume slider + read default interval/volume from plan

* organize choose-plan.ex better

* remove unused vars from tests

* make monthly_cost and yearly_cost nil by default

The actual prices for all plans are stored in Paddle. We don't need to
keep the duplicates in the JSON files.

* add fetch_prices/1 to PaddleApi

* make v4 business ID's differ from growth ones

* render actual price information from plans

...and make the prices in both growth and business plan boxes change
dynamically when the pageview slider or interval is changed.

* highlight current subscription plan box

* add test describe block for business tier subscription

* connect to live socket only on the specific LV page using focus.html

* only wrap the input slider inside the form

* little readability improvement

* add v4 team_member_limits (after rebase with master)

* extract monthly_quota_box function in user_settings

When the business_tier FF is enabled, this section is different and
links to the new upgrade page.

* document subscription statuses

* change _notice.html.eex to .heex

* extract subscription status notice components

* add failed payment notices to upgrade page

* create class_of_element/2 convenience function for testing

* add cancel_subscription mix task

* implement checkout buttons

* mix format

* get all available plans with prices through plans.ex

* use more suitable function for fetching usage

* avoid double db lookups on mount

* rename variable

* separate functions for getting plan by product_id vs subscription

* separate subscription status docs into context module

* consider cancelled subscriptions

* default volume by usage if no subscription plan

* add enterprise-level volume option to slider

* optimize for darkmode

* UI improvements

* display 2 months free notice for yearly billing
* VAT excluded notice
* note about having a business subscription in user settings
* make the page pop and fit plans on screen on first render

* optimize for mobile and remove background containers

* change default price tag to simply 'N/A'

* fix tests

* Change Paddle.js integration to use JavaScript directly
* rename many variables

* allow users on v1 and v2 plan subscribe to 20M and 50M tiers

* add a test for two months free label

* make it work with a free_10k subscription

* small test improvement and formatting

* change other upgrade link in user settings if FF enabled

* dialyzer

* fix typo

* add test for free_10k user

* silence credo

* mix format

* credo - add moduledoc

* credo - another moduledoc

* handle calls to sentry on the api level

* refactor getting regular subscription plan for LiveView

* post review code style tweaks

* remove unused aliases

* credo - add @moduledoc false to Subscriptions

* crash in cancel_subscription task when Repo update fails

* readability improvements (review suggestions)

* add comment about 'external_resource' module attr

---------

Co-authored-by: Vinicius Brasil <vini@hey.com>
2023-10-03 13:36:22 +03:00

123 lines
6.4 KiB
Elixir

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fetch-jsonp/1.1.3/fetch-jsonp.min.js"></script>
<script>
plans = function() {
return {
rawPlans: <%= raw Jason.encode!(Plausible.Billing.Plans.growth_plans_for(@conn.assigns[:current_user])) %>,
localizedPlans: null,
volume: '10k',
billingCycle: 'monthly',
selectedPlanPrice() {
return this.priceFor(this.selectedPlanProductId())
},
selectedPlanProductId() {
var selectedPlan = this.rawPlans.find((plan) => plan.volume === this.volume)
if (this.billingCycle === 'monthly'){
return selectedPlan.monthly_product_id
} else {
return selectedPlan.yearly_product_id
}
},
priceFor(productId) {
var plan = this.localizedPlans.find(plan => plan.product_id === Number(productId))
var currency = {
'USD': '$',
'EUR': '',
'GBP': '£'
}[plan.currency]
return currency + plan.price.net
},
fetchPlans() {
var productIds = []
this.rawPlans.forEach((plan) => {
productIds.push(plan.monthly_product_id)
productIds.push(plan.yearly_product_id)
})
fetchJsonp('https://checkout.paddle.com/api/2.0/prices?product_ids=' + productIds.join(','))
.then((res) => res.json())
.then((data) => {
this.localizedPlans = data.response.products
})
}
}
}
</script>
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black dark:text-gray-100">Change subscription plan</h1>
</div>
<div class="w-full max-w-lg px-4 mx-auto mt-4">
<div x-init="fetchPlans" x-data="plans()" class="flex-1 px-8 py-4 mt-8 mb-4 bg-white rounded shadow-md dark:bg-gray-800">
<div class="w-full pt-2 text-xl font-bold dark:text-gray-100">
Select your new plan
</div>
<div class="w-full text-sm text-gray-500 dark:text-gray-200">
Depending on which plan you choose, your card might be charged immediately and your
next payment date could change. You can preview these changes before committing.
</div>
<div class="pt-8"></div>
<span class="relative z-0 inline-flex w-full shadow-sm">
<button type="button" @click="billingCycle = 'monthly'" :class="{'bg-indigo-600 text-white border-indigo-600': billingCycle === 'monthly', 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-200 border-gray-300 dark:border-gray-500': billingCycle === 'yearly'}" class="relative w-full px-4 py-2 text-sm font-medium text-center border rounded-l-md leading-5 focus:outline-none focus:border-blue-300 focus:ring transition ease-in-out duration-150">
Monthly billing
</button>
<button type="button" @click="billingCycle = 'yearly'" :class="{'bg-indigo-600 text-white border-indigo-600': billingCycle === 'yearly', 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-200 border-gray-300 dark:border-gray-500': billingCycle === 'monthly'}" class="relative w-full px-4 py-2 -ml-px text-sm font-medium text-center border rounded-r-md leading-5 focus:outline-none focus:border-blue-300 focus:ring transition ease-in-out duration-150">
Yearly billing
</button>
</span>
<div class="pt-6"></div>
<template x-if="localizedPlans" class="flex flex-col">
<div class="py-2 -my-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="inline-block min-w-full overflow-hidden align-middle border-b border-gray-200 shadow sm:rounded-lg dark:border-t dark:border-l dark:border-r dark:shadow-none">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase bg-gray-100 border-b border-gray-200 dark:bg-gray-900 leading-4 dark:text-gray-200">
Monthly pageviews
</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase bg-gray-100 border-b border-gray-200 dark:bg-gray-900 leading-4 dark:text-gray-200">
Price
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
<%= for plan <- Plausible.Billing.Plans.growth_plans_for(@conn.assigns[:current_user]) do %>
<%= render("_plan_option.html", plan: plan) %>
<% end %>
</tbody>
</table>
</div>
</div>
</template>
<div x-show="!localizedPlans" class="mx-auto my-56 loading sm"><div></div></div>
<div class="mt-6 text-right">
<span class="inline-flex rounded-md shadow-sm">
<a x-show="selectedPlanProductId() !== '<%= @subscription.paddle_plan_id %>'" style="display: none;" :href="'/billing/change-plan/preview/' + selectedPlanProductId()" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent leading-5 rounded-md hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150">
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path></svg>
Preview changes
</a>
<a x-show="selectedPlanProductId() === '<%= @subscription.paddle_plan_id %>'" style="display: none;" tooltip="Select a different plan to continue" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gray-400 border border-transparent leading-5 rounded-md">
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path></svg>
Preview changes
</a>
</span>
</div>
</div>
</div>
<div class="mt-8 text-center dark:text-gray-100">
Questions? <%= link("Contact us", to: "https://plausible.io/contact", class: "text-indigo-500") %>
</div>
<%= render("_paddle_script.html") %>