billing: Offer release announcement subscriptions.

Also avoid prompting for full name time more than once.
Adds TOS version field to Remote server user.

Co-authored-by: Karl Stolley <karl@zulip.com>
Co-authored-by: Aman Agrawal <amanagr@zulip.com>
This commit is contained in:
Tim Abbott 2023-12-13 12:47:48 -08:00
parent 4ef93de128
commit 1757b88760
8 changed files with 160 additions and 19 deletions

View File

@ -133,6 +133,15 @@ def get_identity_dict_from_signed_access_token(
return identity_dict
def is_tos_consent_needed_for_user(
remote_user: Union[RemoteRealmBillingUser, RemoteServerBillingUser]
) -> bool:
assert settings.TERMS_OF_SERVICE_VERSION is not None
return int(settings.TERMS_OF_SERVICE_VERSION.split(".")[0]) > int(
remote_user.tos_version.split(".")[0]
)
@self_hosting_management_endpoint
@typed_endpoint
def remote_realm_billing_finalize_login(
@ -141,6 +150,8 @@ def remote_realm_billing_finalize_login(
signed_billing_access_token: PathOnly[str],
full_name: Optional[str] = None,
tos_consent: Literal[None, "true"] = None,
enable_major_release_emails: Literal[None, "true", "false"] = None,
enable_maintenance_release_emails: Literal[None, "true", "false"] = None,
) -> HttpResponse:
"""
This is the endpoint accessed via the billing_access_url, generated by
@ -193,9 +204,7 @@ def remote_realm_billing_finalize_login(
remote_realm=remote_realm,
user_uuid=user_uuid,
)
tos_consent_needed = int(settings.TERMS_OF_SERVICE_VERSION.split(".")[0]) > int(
remote_user.tos_version.split(".")[0]
)
tos_consent_needed = is_tos_consent_needed_for_user(remote_user)
except RemoteRealmBillingUser.DoesNotExist:
# This is the first time this user is logging in.
remote_user = None
@ -243,7 +252,7 @@ def remote_realm_billing_finalize_login(
# Users logging in for the first time need to be created and follow
# a different path - they should not be POSTing here. It should be impossible
# to get here with a remote_user that is None without tampering with the form
# or manualling crafting a POST request.
# or manually crafting a POST request.
raise JsonableError(_("User account doesn't exist yet."))
if tos_consent_needed and not tos_consent_given:
@ -251,14 +260,25 @@ def remote_realm_billing_finalize_login(
# don't need a pretty error.
raise JsonableError(_("You must accept the Terms of Service to proceed."))
# The current approach is to update the full_name
# based on what the user entered in the login confirmation form.
# Usually they'll presumably just use the name already set for this object.
# The current approach is to update the full_name and email preferences
# only when the user first logs in.
if full_name is not None:
remote_user.full_name = full_name
remote_user.enable_major_release_emails = enable_major_release_emails == "true"
remote_user.enable_maintenance_release_emails = enable_maintenance_release_emails == "true"
remote_user.tos_version = settings.TERMS_OF_SERVICE_VERSION
remote_user.last_login = timezone_now()
remote_user.save(update_fields=["full_name", "tos_version", "last_login"])
remote_user.save(
update_fields=[
"full_name",
"tos_version",
"last_login",
"enable_maintenance_release_emails",
"enable_major_release_emails",
]
)
identity_dict["remote_billing_user_id"] = remote_user.id
request.session["remote_billing_identities"] = {}
@ -580,6 +600,8 @@ def remote_billing_legacy_server_from_login_confirmation_link(
confirmation_key: PathOnly[str],
full_name: Optional[str] = None,
tos_consent: Literal[None, "true"] = None,
enable_major_release_emails: Literal[None, "true", "false"] = None,
enable_maintenance_release_emails: Literal[None, "true", "false"] = None,
) -> HttpResponse:
"""
The user comes here via the confirmation link they received via email.
@ -602,14 +624,18 @@ def remote_billing_legacy_server_from_login_confirmation_link(
# If this user (identified by email) already did this flow, meaning the have a RemoteServerBillingUser,
# then we don't re-do the ToS consent again.
tos_consent_needed = not RemoteServerBillingUser.objects.filter(
remote_billing_user = RemoteServerBillingUser.objects.filter(
remote_server=remote_server, email=prereg_object.email
).exists()
).first()
tos_consent_needed = remote_billing_user is None or is_tos_consent_needed_for_user(
remote_billing_user
)
if request.method == "GET":
context = {
"remote_server_uuid": remote_server_uuid,
"host": remote_server.hostname,
"user_full_name": getattr(remote_billing_user, "full_name", None),
"user_email": prereg_object.email,
"tos_consent_needed": tos_consent_needed,
"action_url": reverse(
@ -632,12 +658,17 @@ def remote_billing_legacy_server_from_login_confirmation_link(
# don't need a pretty error.
raise JsonableError(_("You must accept the Terms of Service to proceed."))
remote_billing_user, created = RemoteServerBillingUser.objects.update_or_create(
defaults={"full_name": full_name},
email=prereg_object.email,
remote_server=remote_server,
)
if created:
if remote_billing_user is None:
assert full_name is not None
assert settings.TERMS_OF_SERVICE_VERSION is not None
remote_billing_user = RemoteServerBillingUser.objects.create(
full_name=full_name,
email=prereg_object.email,
remote_server=remote_server,
tos_version=settings.TERMS_OF_SERVICE_VERSION,
enable_major_release_emails=enable_major_release_emails == "true",
enable_maintenance_release_emails=enable_maintenance_release_emails == "true",
)
prereg_object.created_user = remote_billing_user
prereg_object.save(update_fields=["created_user"])

View File

@ -26,8 +26,14 @@
{{ csrf_input }}
<div class="input-box remote-billing-confirm-login-form-field">
<label for="full_name" class="inline-block label-title">Full name</label>
<input id="full_name" name="full_name" class="required" type="text" {% if user_full_name %}value="{{ user_full_name }}"{% endif %} />
{% if not user_full_name %}
<input id="full_name" name="full_name" class="required" type="text" />
<div id="remote-billing-confirm-login-form-full_name-error" class="alert alert-danger remote-billing-confirm-login-form-field-error full_name-error"></div>
{% else %}
<div class="not-editable-realm-field">
{{ user_full_name }}
</div>
{% endif %}
</div>
<div class="input-box remote-billing-confirm-login-form-field no-validation">
<label for="user-email" class="inline-block label-title">
@ -37,9 +43,24 @@
{{ user_email }}
</div>
</div>
<!-- user_full_name is not present only when user first logs in which also perfect to set email preferences -->
{% if not user_full_name %}
<div class="input-group remote-billing-confirm-email-subscription-form-field">
<label for="enable-major-release-emails" class="checkbox">
<input id="enable-major-release-emails" name="enable_major_release_emails" type="checkbox" value="true" checked="checked" />
<span></span>
Sign me up for emails about <strong>major Zulip releases</strong> and other big announcements (a few times a year)
</label>
<label for="enable-maintenance-release-emails" class="checkbox">
<input id="enable-maintenance-release-emails" name="enable_maintenance_release_emails" type="checkbox" value="true" checked="checked" />
<span></span>
Sign me up for emails about <strong>all Zulip releases</strong>, including security and maintenance releases (recommended for server administrators)
</label>
</div>
{% endif %}
{% if tos_consent_needed %}
<div class="input-group terms-of-service remote-billing-confirm-login-form-field" id="remote-billing-confirm-login-tos-wrapper">
<label for="remote-billing-confirm-login-tos" class="inline-block checkbox">
<label for="remote-billing-confirm-login-tos" class="checkbox">
<input id="remote-billing-confirm-login-tos" name="tos_consent" class="required" type="checkbox" value="true" />
<span></span>
I agree to the <a href="{{ root_domain_url }}/policies/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a>.

View File

@ -65,6 +65,20 @@ export function initialize(): void {
}
},
});
$<HTMLInputElement>("#enable-major-release-emails").on("change", function () {
if (this.checked) {
$(this).val("true");
}
$(this).val("false");
});
$<HTMLInputElement>("#enable-maintenance-release-emails").on("change", function () {
if (this.checked) {
$(this).val("true");
}
$(this).val("false");
});
}
$(() => {

View File

@ -731,7 +731,7 @@ input[name="licenses"] {
}
#remote-billing-confirm-login-form #remote-billing-confirm-login-tos-wrapper {
margin: 25px auto 10px;
margin: 0 auto 10px;
}
#account-deactivated-success-page-details

View File

@ -1147,6 +1147,23 @@ button#register_auth_button_gitlab {
}
}
.remote-billing-confirm-email-subscription-form-field {
width: 450px;
margin-top: 15px;
.checkbox {
display: block;
/* Present a hanging indent on subscription copy
and checkboxes. */
text-indent: -26px;
margin: 0 0 10px 26px;
}
strong {
font-weight: 600;
}
}
.org-url {
margin-bottom: 5px !important;
}

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.8 on 2023-12-14 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zilencer", "0053_remoterealmauditlog_acting_remote_user_and_more"),
]
operations = [
migrations.AddField(
model_name="remoterealmbillinguser",
name="enable_maintenance_release_emails",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="remoterealmbillinguser",
name="enable_major_release_emails",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="remoteserverbillinguser",
name="enable_maintenance_release_emails",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="remoteserverbillinguser",
name="enable_major_release_emails",
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.8 on 2023-12-14 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zilencer", "0054_remoterealmbillinguser_enable_maintenance_release_emails_and_more"),
]
operations = [
migrations.AddField(
model_name="remoteserverbillinguser",
name="tos_version",
field=models.TextField(default="-1"),
),
]

View File

@ -195,6 +195,9 @@ class RemoteRealmBillingUser(AbstractRemoteRealmBillingUser):
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
enable_major_release_emails = models.BooleanField(default=True)
enable_maintenance_release_emails = models.BooleanField(default=True)
class Meta:
unique_together = [
("remote_realm", "user_uuid"),
@ -233,6 +236,12 @@ class RemoteServerBillingUser(AbstractRemoteServerBillingUser):
is_active = models.BooleanField(default=True)
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
enable_major_release_emails = models.BooleanField(default=True)
enable_maintenance_release_emails = models.BooleanField(default=True)
class Meta:
unique_together = [
("remote_server", "email"),