Remove trial banner for admins & viewers (#1308)

* Start trial only when the user creates a site

* End trial when ownership is transfered
This commit is contained in:
Uku Taht 2021-09-08 15:15:37 +03:00 committed by GitHub
parent c8a1b5c73c
commit 700a65c98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 239 additions and 10 deletions

View File

@ -37,14 +37,14 @@ defmodule Plausible.Auth.User do
|> validate_length(:password, max: 64, message: "cannot be longer than 64 characters") |> validate_length(:password, max: 64, message: "cannot be longer than 64 characters")
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> hash_password() |> hash_password()
|> change(trial_expiry_date: trial_expiry()) |> start_trial
|> unique_constraint(:email) |> unique_constraint(:email)
end end
def changeset(user, attrs \\ %{}) do def changeset(user, attrs \\ %{}) do
user user
|> cast(attrs, [:email, :name, :email_verified, :theme, :trial_expiry_date]) |> cast(attrs, [:email, :name, :email_verified, :theme, :trial_expiry_date])
|> validate_required([:email, :name, :email_verified, :trial_expiry_date]) |> validate_required([:email, :name, :email_verified])
|> unique_constraint(:email) |> unique_constraint(:email)
end end
@ -65,6 +65,18 @@ defmodule Plausible.Auth.User do
def hash_password(changeset), do: changeset def hash_password(changeset), do: changeset
def remove_trial_expiry(user) do
change(user, trial_expiry_date: nil)
end
def start_trial(user) do
change(user, trial_expiry_date: trial_expiry())
end
def end_trial(user) do
change(user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
end
defp trial_expiry() do defp trial_expiry() do
if Application.get_env(:plausible, :is_selfhost) do if Application.get_env(:plausible, :is_selfhost) do
Timex.today() |> Timex.shift(years: 100) Timex.today() |> Timex.shift(years: 100)

View File

@ -104,6 +104,8 @@ defmodule Plausible.Billing do
PaddleApi.update_subscription_preview(subscription.paddle_subscription_id, new_plan_id) PaddleApi.update_subscription_preview(subscription.paddle_subscription_id, new_plan_id)
end end
def needs_to_upgrade?(%Plausible.Auth.User{trial_expiry_date: nil}), do: true
def needs_to_upgrade?(user) do def needs_to_upgrade?(user) do
if Timex.before?(user.trial_expiry_date, Timex.today()) do if Timex.before?(user.trial_expiry_date, Timex.today()) do
!subscription_is_active?(user.subscription) !subscription_is_active?(user.subscription)
@ -122,6 +124,8 @@ defmodule Plausible.Billing do
defp subscription_is_active?(%Subscription{}), do: false defp subscription_is_active?(%Subscription{}), do: false
defp subscription_is_active?(nil), do: false defp subscription_is_active?(nil), do: false
def on_trial?(%Plausible.Auth.User{trial_expiry_date: nil}), do: false
def on_trial?(user) do def on_trial?(user) do
!subscription_is_active?(user.subscription) && trial_days_left(user) >= 0 !subscription_is_active?(user.subscription) && trial_days_left(user) >= 0
end end

View File

@ -22,10 +22,22 @@ defmodule Plausible.Sites do
repo.insert(membership_changeset) repo.insert(membership_changeset)
end) end)
|> maybe_start_trial(user)
|> Repo.transaction() |> Repo.transaction()
end end
end end
defp maybe_start_trial(multi, user) do
case user.trial_expiry_date do
nil ->
changeset = Plausible.Auth.User.start_trial(user)
Ecto.Multi.update(multi, :user, changeset)
_ ->
multi
end
end
def has_stats?(site) do def has_stats?(site) do
if site.has_stats do if site.has_stats do
true true

View File

@ -98,6 +98,12 @@ defmodule PlausibleWeb.AuthController do
invitation = Repo.get_by(Plausible.Auth.Invitation, invitation_id: invitation_id) invitation = Repo.get_by(Plausible.Auth.Invitation, invitation_id: invitation_id)
user = Plausible.Auth.User.new(params["user"]) user = Plausible.Auth.User.new(params["user"])
user =
case invitation.role do
:owner -> user
_ -> Plausible.Auth.User.remove_trial_expiry(user)
end
if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do
case Repo.insert(user) do case Repo.insert(user) do
{:ok, user} -> {:ok, user} ->

View File

@ -17,7 +17,9 @@ defmodule PlausibleWeb.InvitationController do
multi = multi =
if invitation.role == :owner do if invitation.role == :owner do
downgrade_previous_owner(Multi.new(), invitation.site) Multi.new()
|> downgrade_previous_owner(invitation.site)
|> end_trial_of_new_owner(user)
else else
Multi.new() Multi.new()
end end
@ -35,9 +37,10 @@ defmodule PlausibleWeb.InvitationController do
|> Multi.delete(:invitation, invitation) |> Multi.delete(:invitation, invitation)
case Repo.transaction(multi) do case Repo.transaction(multi) do
{:ok, _} -> {:ok, changes} ->
updated_user = Map.get(changes, :user, user)
notify_invitation_accepted(invitation) notify_invitation_accepted(invitation)
Plausible.Billing.SiteLocker.check_sites_for(user) Plausible.Billing.SiteLocker.check_sites_for(updated_user)
conn conn
|> put_flash(:success, "You now have access to #{invitation.site.domain}") |> put_flash(:success, "You now have access to #{invitation.site.domain}")
@ -61,6 +64,14 @@ defmodule PlausibleWeb.InvitationController do
Multi.update_all(multi, :prev_owner, prev_owner, set: [role: :admin]) Multi.update_all(multi, :prev_owner, prev_owner, set: [role: :admin])
end end
defp end_trial_of_new_owner(multi, new_owner) do
if Plausible.Billing.on_trial?(new_owner) do
Ecto.Multi.update(multi, :user, Plausible.Auth.User.end_trial(new_owner))
else
multi
end
end
def reject_invitation(conn, %{"invitation_id" => invitation_id}) do def reject_invitation(conn, %{"invitation_id" => invitation_id}) do
invitation = invitation =
Repo.get_by!(Invitation, invitation_id: invitation_id) Repo.get_by!(Invitation, invitation_id: invitation_id)

View File

@ -50,11 +50,14 @@ defmodule PlausibleWeb.SiteController do
end end
def new(conn, _params) do def new(conn, _params) do
current_user = conn.assigns[:current_user] current_user = conn.assigns[:current_user] |> Repo.preload(site_memberships: :site)
site_count = Enum.count(Plausible.Sites.owned_by(current_user))
owned_site_count =
current_user.site_memberships |> Enum.filter(fn m -> m.role == :owner end) |> Enum.count()
site_limit = Plausible.Billing.sites_limit(current_user) site_limit = Plausible.Billing.sites_limit(current_user)
is_at_limit = site_limit && site_count >= site_limit is_at_limit = site_limit && owned_site_count >= site_limit
is_first_site = site_count == 0 is_first_site = Enum.empty?(current_user.site_memberships)
changeset = Plausible.Site.changeset(%Plausible.Site{}) changeset = Plausible.Site.changeset(%Plausible.Site{})

View File

@ -159,7 +159,15 @@
You've been invited to the <span x-text="selectedInvitation && selectedInvitation.site.domain"></span> analytics dashboard as <b class="capitalize" x-text="selectedInvitation && selectedInvitation.role">Admin</b>. You've been invited to the <span x-text="selectedInvitation && selectedInvitation.site.domain"></span> analytics dashboard as <b class="capitalize" x-text="selectedInvitation && selectedInvitation.role">Admin</b>.
</p> </p>
<p x-show="selectedInvitation && selectedInvitation.role === 'owner'" class="mt-2 text-sm text-gray-500"> <p x-show="selectedInvitation && selectedInvitation.role === 'owner'" class="mt-2 text-sm text-gray-500">
If you accept the ownership transfer, you will be responsible for billing. If you accept the ownership transfer, you will be responsible for billing going forward.
<%= if is_nil(@current_user.trial_expiry_date) && is_nil(@current_user.subscription) do %>
<br/><br />
You will have to enter your card details immediately with no 30-day trial.
<% end %>
<%= if Plausible.Billing.on_trial?(@current_user) do %>
<br/><br />
Your 30-day free trial will end immediately and you will have to enter your card details to keep using Plausible.
<% end %>
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex"> <div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @changeset, "/sites", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> <%= form_for @changeset, "/sites", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100">Your website details</h2> <h2 class="text-xl font-black dark:text-gray-100">Your website details</h2>
<%= if @is_at_limit do %> <%= if @is_at_limit do %>
<div class="rounded-md bg-yellow-50 dark:bg-transparent dark:border border-yellow-200 p-4 mt-4"> <div class="rounded-md bg-yellow-50 dark:bg-transparent dark:border border-yellow-200 p-4 mt-4">
<div class="flex"> <div class="flex">
@ -22,6 +23,26 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<%= if is_nil(@current_user.trial_expiry_date) do %>
<div class="rounded-md bg-blue-50 dark:bg-transparent dark:border border-blue-200 p-4 mt-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-500 dark:text-blue-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<div class="text-sm text-blue-700 dark:text-blue-300">
<p>
When you create your first site, your account will enter a 30 day free trial.
</p>
</div>
</div>
</div>
</div>
<% end %>
<div class="my-6"> <div class="my-6">
<%= label f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> <%= label f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1">Just the naked domain or subdomain without 'www'</p> <p class="text-gray-500 dark:text-gray-400 text-xs mt-1">Just the naked domain or subdomain without 'www'</p>

View File

@ -0,0 +1,9 @@
defmodule Plausible.Repo.Migrations.AllowTrialExpiryToBeNull do
use Ecto.Migration
def change do
alter table(:users) do
modify :trial_expiry_date, :date, null: true
end
end
end

View File

@ -71,6 +71,116 @@ defmodule PlausibleWeb.AuthControllerTest do
end end
end end
describe "GET /register/invitations/:invitation_id" do
test "shows the register form", %{conn: conn} do
inviter = insert(:user)
site = insert(:site, members: [inviter])
invitation =
insert(:invitation,
site_id: site.id,
inviter: inviter,
email: "user@email.co",
role: :admin
)
conn = get(conn, "/register/invitation/#{invitation.invitation_id}")
assert html_response(conn, 200) =~ "Enter your details"
end
end
describe "POST /register/invitation/:invitation_id" do
setup do
inviter = insert(:user)
site = insert(:site, members: [inviter])
invitation =
insert(:invitation,
site_id: site.id,
inviter: inviter,
email: "user@email.co",
role: :admin
)
{:ok, %{site: site, invitation: invitation}}
end
test "registering sends an activation link", %{conn: conn, invitation: invitation} do
post(conn, "/register/invitation/#{invitation.invitation_id}",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject})
assert user_email == "user@example.com"
assert subject =~ "is your Plausible email verification code"
end
test "creates user record", %{conn: conn, invitation: invitation} do
post(conn, "/register/invitation/#{invitation.invitation_id}",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
user = Repo.get_by(Plausible.Auth.User, email: "user@example.com")
assert user.name == "Jane Doe"
end
test "leaves trial_expiry_date null when invitation role is not :owner", %{
conn: conn,
invitation: invitation
} do
post(conn, "/register/invitation/#{invitation.invitation_id}",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
user = Repo.get_by(Plausible.Auth.User, email: "user@example.com")
assert is_nil(user.trial_expiry_date)
end
test "logs the user in", %{conn: conn, invitation: invitation} do
conn =
post(conn, "/register/invitation/#{invitation.invitation_id}",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
assert get_session(conn, :current_user_id)
end
test "user is redirected to activation after registration", %{conn: conn} do
conn =
post(conn, "/register",
user: %{
name: "Jane Doe",
email: "user@example.com",
password: "very-secret",
password_confirmation: "very-secret"
}
)
assert redirected_to(conn) == "/activate"
end
end
describe "GET /activate" do describe "GET /activate" do
setup [:create_user, :log_in] setup [:create_user, :log_in]

View File

@ -101,6 +101,26 @@ defmodule PlausibleWeb.Site.InvitationControllerTest do
assert Repo.reload!(site).locked assert Repo.reload!(site).locked
end end
test "ownership transfer - will end the trial of the new owner immediately", %{
conn: conn,
user: user
} do
Repo.update_all(from(u in Plausible.Auth.User, where: u.id == ^user.id),
set: [trial_expiry_date: Timex.today() |> Timex.shift(days: 7)]
)
inviter = insert(:user)
site = insert(:site, locked: false)
invitation =
insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :owner)
post(conn, "/sites/invitations/#{invitation.invitation_id}/accept")
assert Timex.before?(Repo.reload!(user).trial_expiry_date, Timex.today())
assert Repo.reload!(site).locked
end
end end
describe "POST /sites/invitations/:invitation_id/reject" do describe "POST /sites/invitations/:invitation_id/reject" do

View File

@ -104,6 +104,19 @@ defmodule PlausibleWeb.SiteControllerTest do
assert Repo.exists?(Plausible.Site, domain: "example.com") assert Repo.exists?(Plausible.Site, domain: "example.com")
end end
test "starts trial if user does not have trial yet", %{conn: conn, user: user} do
Plausible.Auth.User.remove_trial_expiry(user) |> Repo.update!()
post(conn, "/sites", %{
"site" => %{
"domain" => "example.com",
"timezone" => "Europe/London"
}
})
assert Repo.reload!(user).trial_expiry_date
end
test "sends welcome email if this is the user's first site", %{conn: conn} do test "sends welcome email if this is the user's first site", %{conn: conn} do
post(conn, "/sites", %{ post(conn, "/sites", %{
"site" => %{ "site" => %{