plans: Show buttons as per current context.

Also show correct tab based on remote / cloud user.
This commit is contained in:
Aman Agrawal 2023-12-05 06:42:52 +00:00 committed by Tim Abbott
parent 49908ba166
commit 8d9a7679bc
9 changed files with 248 additions and 81 deletions

View File

@ -106,8 +106,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
# Go to the URL we're redirected to after authentication and assert
# some basic expected content.
result = self.client_get(result["Location"], subdomain="selfhosting")
self.assert_in_success_response(["Your remote user info:"], result)
self.assert_in_success_response([desdemona.delivery_email], result)
self.assert_in_success_response(["showing-self-hosted", "Retain full control"], result)
@responses.activate
def test_remote_billing_authentication_flow_realm_not_registered(self) -> None:
@ -143,8 +142,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
self.assertEqual(result["Location"], f"/realm/{realm.uuid!s}/plans/")
result = self.client_get(result["Location"], subdomain="selfhosting")
self.assert_in_success_response(["Your remote user info:"], result)
self.assert_in_success_response([desdemona.delivery_email], result)
self.assert_in_success_response(["showing-self-hosted", "Retain full control"], result)
@responses.activate
def test_remote_billing_authentication_flow_tos_consent_failure(self) -> None:
@ -227,8 +225,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
tick=False,
):
result = self.client_get(final_url, subdomain="selfhosting")
self.assert_in_success_response(["Your remote user info:"], result)
self.assert_in_success_response([desdemona.delivery_email], result)
self.assert_in_success_response(["showing-self-hosted", "Retain full control"], result)
# Now go there again, simulating doing this after the session has expired.
# We should be denied access and redirected to re-auth.
@ -257,8 +254,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
)
self.assertEqual(result["Location"], f"/realm/{realm.uuid!s}/plans/")
result = self.client_get(result["Location"], subdomain="selfhosting")
self.assert_in_success_response(["Your remote user info:"], result)
self.assert_in_success_response([desdemona.delivery_email], result)
self.assert_in_success_response(["showing-self-hosted", "Retain full control"], result)
@responses.activate
def test_remote_billing_unauthed_access(self) -> None:
@ -408,7 +404,7 @@ class LegacyServerLoginTest(BouncerTestCase):
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], f"/server/{self.uuid}/upgrade/")
self.assertEqual(result["Location"], f"/server/{self.uuid}/plans/")
# Verify the authed data that should have been stored in the session.
identity_dict = LegacyServerIdentityDict(

View File

@ -27,13 +27,13 @@ from corporate.views.portico import (
hello_view,
landing_view,
plans_view,
remote_realm_plans_page,
remote_server_plans_page,
team_view,
)
from corporate.views.remote_billing_page import (
remote_billing_legacy_server_login,
remote_realm_billing_finalize_login,
remote_realm_plans_page,
remote_server_plans_page,
)
from corporate.views.session import (
start_card_update_stripe_session,

View File

@ -1,3 +1,4 @@
from dataclasses import asdict, dataclass
from typing import Optional
from urllib.parse import urlencode
@ -6,9 +7,14 @@ from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from corporate.lib.stripe import is_realm_on_free_trial
from corporate.models import get_customer_by_realm
from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint,
authenticated_remote_server_management_endpoint,
)
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm
from zerver.context_processors import get_realm_from_request, latest_info_context
from zerver.decorator import add_google_analytics
from zerver.lib.github import (
@ -47,16 +53,43 @@ def app_download_link_redirect(request: HttpRequest, platform: str) -> HttpRespo
return TemplateResponse(request, "404.html", status=404)
def is_customer_on_free_trial(customer_plan: CustomerPlan) -> bool:
return customer_plan.status in (
CustomerPlan.FREE_TRIAL,
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
)
@dataclass
class PlansPageContext:
sponsorship_url: str
free_trial_days: Optional[int]
on_free_trial: bool = False
sponsorship_pending: bool = False
is_sponsored: bool = False
is_cloud_realm: bool = False
is_self_hosted_realm: bool = False
is_new_customer: bool = False
on_free_tier: bool = False
customer_plan: Optional[CustomerPlan] = None
billing_base_url: str = ""
@add_google_analytics
def plans_view(request: HttpRequest) -> HttpResponse:
realm = get_realm_from_request(request)
free_trial_days = settings.FREE_TRIAL_DAYS
sponsorship_pending = False
sponsorship_url = "/sponsorship/"
context = PlansPageContext(
is_cloud_realm=True,
sponsorship_url=reverse("sponsorship_request"),
free_trial_days=settings.FREE_TRIAL_DAYS,
is_sponsored=realm is not None and realm.plan_type == Realm.PLAN_TYPE_STANDARD_FREE,
)
if is_subdomain_root_or_alias(request):
# If we're on the root domain, we make this link first ask you which organization.
sponsorship_url = f"/accounts/go/?{urlencode({'next': sponsorship_url})}"
realm_on_free_trial = False
context.sponsorship_url = f"/accounts/go/?{urlencode({'next': context.sponsorship_url})}"
if realm is not None:
if realm.plan_type == Realm.PLAN_TYPE_SELF_HOSTED and settings.PRODUCTION:
@ -65,21 +98,99 @@ def plans_view(request: HttpRequest) -> HttpResponse:
return redirect_to_login(next="/plans/")
if request.user.is_guest:
return TemplateResponse(request, "404.html", status=404)
customer = get_customer_by_realm(realm)
if customer is not None:
sponsorship_pending = customer.sponsorship_pending
realm_on_free_trial = is_realm_on_free_trial(realm)
customer = get_customer_by_realm(realm)
context.on_free_tier = customer is None and not context.is_sponsored
if customer is not None:
context.sponsorship_pending = customer.sponsorship_pending
context.customer_plan = get_current_plan_by_customer(customer)
if context.customer_plan is None:
# Cloud realms on free tier don't have active customer plan unless they are sponsored.
context.on_free_tier = not context.is_sponsored
else:
context.on_free_trial = is_customer_on_free_trial(context.customer_plan)
context.is_new_customer = (
not context.on_free_tier and context.customer_plan is None and not context.is_sponsored
)
return TemplateResponse(
request,
"corporate/plans.html",
context={
"realm": realm,
"free_trial_days": free_trial_days,
"realm_on_free_trial": realm_on_free_trial,
"sponsorship_pending": sponsorship_pending,
"sponsorship_url": sponsorship_url,
},
context=asdict(context),
)
@add_google_analytics
@authenticated_remote_realm_management_endpoint
def remote_realm_plans_page(
request: HttpRequest, billing_session: RemoteRealmBillingSession
) -> HttpResponse: # nocoverage
customer = billing_session.get_customer()
context = PlansPageContext(
is_self_hosted_realm=True,
sponsorship_url=reverse(
"remote_realm_sponsorship_page", args=(billing_session.remote_realm.uuid,)
),
free_trial_days=settings.FREE_TRIAL_DAYS,
billing_base_url=billing_session.billing_base_url,
is_sponsored=billing_session.is_sponsored(),
)
context.on_free_tier = customer is None and not context.is_sponsored
if customer is not None:
context.sponsorship_pending = customer.sponsorship_pending
context.customer_plan = get_current_plan_by_customer(customer)
if context.customer_plan is None:
context.on_free_tier = not context.is_sponsored
else:
context.on_free_trial = is_customer_on_free_trial(context.customer_plan)
context.is_new_customer = (
not context.on_free_tier and context.customer_plan is None and not context.is_sponsored
)
return TemplateResponse(
request,
"corporate/plans.html",
context=asdict(context),
)
@add_google_analytics
@authenticated_remote_server_management_endpoint
def remote_server_plans_page(
request: HttpRequest, billing_session: RemoteServerBillingSession
) -> HttpResponse: # nocoverage
customer = billing_session.get_customer()
context = PlansPageContext(
is_self_hosted_realm=True,
sponsorship_url=reverse(
"remote_server_sponsorship_page", args=(billing_session.remote_server.uuid,)
),
free_trial_days=settings.FREE_TRIAL_DAYS,
billing_base_url=billing_session.billing_base_url,
is_sponsored=billing_session.is_sponsored(),
)
context.on_free_tier = customer is None and not context.is_sponsored
if customer is not None:
context.sponsorship_pending = customer.sponsorship_pending
context.customer_plan = get_current_plan_by_customer(customer)
if context.customer_plan is None:
context.on_free_tier = not context.is_sponsored
else:
context.on_free_tier = context.customer_plan.tier in (
CustomerPlan.TIER_SELF_HOSTED_LEGACY,
CustomerPlan.TIER_SELF_HOSTED_BASE,
)
context.on_free_trial = is_customer_on_free_trial(context.customer_plan)
context.is_new_customer = (
not context.on_free_tier and context.customer_plan is None and not context.is_sponsored
)
return TemplateResponse(
request,
"corporate/plans.html",
context=asdict(context),
)

View File

@ -13,11 +13,7 @@ from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt
from pydantic import Json
from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint,
authenticated_remote_server_management_endpoint,
self_hosting_management_endpoint,
)
from corporate.lib.decorator import self_hosting_management_endpoint
from corporate.lib.remote_billing_util import (
REMOTE_BILLING_SESSION_VALIDITY_SECONDS,
LegacyServerIdentityDict,
@ -25,7 +21,6 @@ from corporate.lib.remote_billing_util import (
RemoteBillingUserDict,
get_identity_dict_from_session,
)
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError
from zerver.lib.remote_server import RealmDataForAnalytics, UserDataForRemoteBilling
from zerver.lib.response import json_success
@ -293,22 +288,6 @@ def remote_billing_plans_common(
return render_tmp_remote_billing_page(request, realm_uuid=realm_uuid, server_uuid=server_uuid)
@authenticated_remote_realm_management_endpoint
def remote_realm_plans_page(
request: HttpRequest, billing_session: RemoteRealmBillingSession
) -> HttpResponse:
realm_uuid = str(billing_session.remote_realm.uuid)
return remote_billing_plans_common(request, realm_uuid=realm_uuid, server_uuid=None)
@authenticated_remote_server_management_endpoint
def remote_server_plans_page(
request: HttpRequest, billing_session: RemoteServerBillingSession
) -> HttpResponse:
server_uuid = str(billing_session.remote_server.uuid)
return remote_billing_plans_common(request, server_uuid=server_uuid, realm_uuid=None)
def remote_billing_page_common(
request: HttpRequest, realm_uuid: Optional[str], server_uuid: Optional[str]
) -> HttpResponse:
@ -369,10 +348,7 @@ def remote_billing_legacy_server_login(
reverse(f"remote_server_{next_page}_page", args=(remote_server_uuid,))
)
elif remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_HOSTED:
# TODO: Take user to plans page once that is available.
return HttpResponseRedirect(
reverse("remote_server_upgrade_page", args=(remote_server_uuid,))
)
return HttpResponseRedirect(reverse("remote_server_plans_page", args=(remote_server_uuid,)))
elif remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_COMMUNITY:
return HttpResponseRedirect(
reverse("remote_server_sponsorship_page", args=(remote_server_uuid,))

View File

@ -13,7 +13,7 @@
{% include 'zerver/landing_nav.html' %}
<div class="portico-pricing plans showing-cloud">
<div class="portico-pricing plans {% if is_self_hosted_realm %} showing-self-hosted {% else %} showing-cloud {% endif %}">
<div class="main">
{% include "corporate/pricing_model.html" %}
</div>

View File

@ -33,15 +33,15 @@
</div>
<div class="bottom">
<div class="text-content">
{% if not realm or realm.plan_type == realm.PLAN_TYPE_SELF_HOSTED %}
<a href="/new/" class="button get-started-button">
Create organization
</a>
{% elif realm.plan_type == realm.PLAN_TYPE_LIMITED or sponsorship_pending %}
{% if is_cloud_realm and on_free_tier %}
<div class="pricing-details"></div>
<a href='/upgrade/' class="button current-plan-button" type="button">
Current plan
</a>
{% elif not is_cloud_realm or is_new_customer %}
<a href="/new/" class="button get-started-button">
Create organization
</a>
{% endif %}
</div>
</div>
@ -69,7 +69,7 @@
</p>
</div>
</div>
{% if not realm %}
{% if is_cloud_realm and on_free_tier and not sponsorship_pending %}
<a href="/upgrade/" class="button upgrade-button">
{% if free_trial_days %}
Start {{ free_trial_days }}-day free trial
@ -77,16 +77,17 @@
Upgrade to Standard
{% endif %}
</a>
{% elif realm.plan_type in [realm.PLAN_TYPE_STANDARD, realm.PLAN_TYPE_STANDARD_FREE] %}
<!-- Sponsored realm may not have customer plan. -->
{% elif (is_cloud_realm and is_sponsored) or (customer_plan and customer_plan.tier == customer_plan.TIER_CLOUD_STANDARD) %}
<a href='/billing' class="button current-plan-button" type="button">
<i class="icon current-plan-icon"></i>
{% if realm_on_free_trial %}
{% if on_free_trial %}
Current plan (free trial)
{% else %}
Current plan
{% endif %}
</a>
{% elif sponsorship_pending %}
{% elif is_cloud_realm and sponsorship_pending %}
<a href="/billing/" class="button current-plan-button" type="button">
Sponsorship pending
</a>
@ -158,13 +159,80 @@
</div>
<div class="bottom">
<div class="text-content">
{% if is_self_hosted_realm and on_free_tier %}
<a href='{{ billing_base_url }}/billing' class="button current-plan-button" type="button">
<i class="icon current-plan-icon"></i>
Current plan
</a>
{% elif not is_self_hosted_realm %}
<a href="/self-hosting/" class="button get-started-button">
Self-host Zulip
</a>
{% endif %}
</div>
</div>
</div>
{% if development_environment %}
<div class="price-box" tabindex="-1">
<div class="text-content">
<h2>Business</h2>
<ul class="feature-list">
<li><span>All Free features included</span></li>
<li><span>Professional support with SLAs</span></li>
<li><span>High availability</span></li>
<li><span>Incident collaboration</span></li>
<li><span>Advanced compliance</span></li>
<li><span>Funds the Zulip open source project</span></li>
</ul>
</div>
<div class="bottom">
<div class="text-content">
{% if is_self_hosted_realm and on_free_tier and not sponsorship_pending %}
<a href="{{ billing_base_url }}/sponsorship/" class="button current-plan-button request-sponsorship">
Request sponsorship
</a>
<a href="{{ billing_base_url }}/upgrade/" class="button upgrade-button">
{% if free_trial_days %}
Start {{ free_trial_days }}-day free trial
{% else %}
Upgrade to Business
{% endif %}
</a>
{% elif is_self_hosted_realm and (is_sponsored or (customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BUSINESS)) %}
<a href='{{ billing_base_url }}/billing' class="button current-plan-button" type="button">
<i class="icon current-plan-icon"></i>
{% if on_free_trial %}
Current plan (free trial)
{% else %}
Current plan
{% endif %}
</a>
{% elif is_self_hosted_realm and sponsorship_pending %}
<a href="{{ billing_base_url }}/billing/" class="button current-plan-button" type="button">
Sponsorship pending
</a>
{% elif is_self_hosted_realm %}
<a href="{{ billing_base_url }}/sponsorship/" class="button upgrade-button request-sponsorship">
Request sponsorship
</a>
<a href="{{ billing_base_url }}/upgrade/" class="button upgrade-button">
{% if free_trial_days %}
Start {{ free_trial_days }}-day free trial
{% else %}
Upgrade to Business
{% endif %}
</a>
{% else %}
<a href="mailto:sales@zulip.com" target="_blank" rel="noopener noreferrer" class="button upgrade-button">
Contact sales
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="price-box" tabindex="-1">
<div class="text-content">
<h2>Enterprise</h2>
@ -179,9 +247,16 @@
</div>
<div class="bottom">
<div class="text-content">
{% if is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_ENTERPRISE %}
<a href='{{ billing_base_url }}/billing' class="button current-plan-button" type="button">
<i class="icon current-plan-icon"></i>
Current plan
</a>
{% else %}
<a href="mailto:sales@zulip.com" target="_blank" rel="noopener noreferrer" class="button upgrade-button">
Contact sales
</a>
{% endif %}
</div>
</div>
</div>

View File

@ -136,12 +136,18 @@ $(() => {
render_tabs(contributors);
}
if (window.location.pathname === "/plans/" && window.location.hash === "#self-hosted") {
if (window.location.pathname.endsWith("/plans/")) {
const tabs = ["#cloud", "#self-hosted"];
if (!tabs.includes(window.location.hash)) {
return;
}
const tab_to_show = window.location.hash;
// Don't scroll to the target element
window.scroll({top: 0});
const $pricing_wrapper = $(".portico-pricing");
$pricing_wrapper.removeClass("showing-cloud");
$pricing_wrapper.addClass("showing-self-hosted");
$pricing_wrapper.removeClass("showing-cloud showing-self-hosted");
$pricing_wrapper.addClass(`showing-${tab_to_show.slice(1)}`);
}
});

View File

@ -145,6 +145,10 @@
}
}
.request-sponsorship {
margin-bottom: 10px;
}
.pricing-pane-scroll-container {
grid-area: pricing;
overflow-x: auto;

View File

@ -562,11 +562,6 @@ class PlansPageTest(ZulipTestCase):
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "https://zulip.com/plans/")
# But in the development environment, it renders a page
result = self.client_get("/plans/", subdomain="zulip")
self.assert_in_success_response([sign_up_now, upgrade_to_standard], result)
self.assert_not_in_success_response([current_plan, sponsorship_pending], result)
realm.plan_type = Realm.PLAN_TYPE_LIMITED
realm.save(update_fields=["plan_type"])
result = self.client_get("/plans/", subdomain="zulip")
@ -580,6 +575,8 @@ class PlansPageTest(ZulipTestCase):
[sign_up_now, sponsorship_pending, upgrade_to_standard], result
)
# Sponsored realms always have Customer entry.
customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
realm.save(update_fields=["plan_type"])
result = self.client_get("/plans/", subdomain="zulip")
@ -588,6 +585,14 @@ class PlansPageTest(ZulipTestCase):
[sign_up_now, upgrade_to_standard, sponsorship_pending], result
)
plan = CustomerPlan.objects.create(
customer=customer,
tier=CustomerPlan.TIER_CLOUD_STANDARD,
status=CustomerPlan.ACTIVE,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
)
realm.plan_type = Realm.PLAN_TYPE_STANDARD
realm.save(update_fields=["plan_type"])
result = self.client_get("/plans/", subdomain="zulip")
@ -596,14 +601,8 @@ class PlansPageTest(ZulipTestCase):
[sign_up_now, upgrade_to_standard, sponsorship_pending], result
)
customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
plan = CustomerPlan.objects.create(
customer=customer,
tier=CustomerPlan.TIER_CLOUD_STANDARD,
status=CustomerPlan.FREE_TRIAL,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY,
)
plan.status = CustomerPlan.FREE_TRIAL
plan.save(update_fields=["status"])
result = self.client_get("/plans/", subdomain="zulip")
self.assert_in_success_response(["Current plan (free trial)"], result)
self.assert_not_in_success_response(