From 700a65c98a112a201788b941d74cf05511776168 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Wed, 8 Sep 2021 15:15:37 +0300 Subject: [PATCH] Remove trial banner for admins & viewers (#1308) * Start trial only when the user creates a site * End trial when ownership is transfered --- lib/plausible/auth/user.ex | 16 ++- lib/plausible/billing/billing.ex | 4 + lib/plausible/sites.ex | 12 ++ .../controllers/auth_controller.ex | 6 + .../controllers/invitation_controller.ex | 17 ++- .../controllers/site_controller.ex | 11 +- .../templates/site/index.html.eex | 10 +- lib/plausible_web/templates/site/new.html.eex | 21 ++++ ...08081119_allow_trial_expiry_to_be_null.exs | 9 ++ .../controllers/auth_controller_test.exs | 110 ++++++++++++++++++ .../invitation_controller_test.exs | 20 ++++ .../controllers/site_controller_test.exs | 13 +++ 12 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 priv/repo/migrations/20210908081119_allow_trial_expiry_to_be_null.exs diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex index 1c163fe5f..05e26ab40 100644 --- a/lib/plausible/auth/user.ex +++ b/lib/plausible/auth/user.ex @@ -37,14 +37,14 @@ defmodule Plausible.Auth.User do |> validate_length(:password, max: 64, message: "cannot be longer than 64 characters") |> validate_confirmation(:password) |> hash_password() - |> change(trial_expiry_date: trial_expiry()) + |> start_trial |> unique_constraint(:email) end def changeset(user, attrs \\ %{}) do user |> 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) end @@ -65,6 +65,18 @@ defmodule Plausible.Auth.User do 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 if Application.get_env(:plausible, :is_selfhost) do Timex.today() |> Timex.shift(years: 100) diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index d43832912..e754b5ada 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -104,6 +104,8 @@ defmodule Plausible.Billing do PaddleApi.update_subscription_preview(subscription.paddle_subscription_id, new_plan_id) end + def needs_to_upgrade?(%Plausible.Auth.User{trial_expiry_date: nil}), do: true + def needs_to_upgrade?(user) do if Timex.before?(user.trial_expiry_date, Timex.today()) do !subscription_is_active?(user.subscription) @@ -122,6 +124,8 @@ defmodule Plausible.Billing do defp subscription_is_active?(%Subscription{}), 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 !subscription_is_active?(user.subscription) && trial_days_left(user) >= 0 end diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index e073fc7f1..a57df0b48 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -22,10 +22,22 @@ defmodule Plausible.Sites do repo.insert(membership_changeset) end) + |> maybe_start_trial(user) |> Repo.transaction() 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 if site.has_stats do true diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index 90ba33278..011beec50 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -98,6 +98,12 @@ defmodule PlausibleWeb.AuthController do invitation = Repo.get_by(Plausible.Auth.Invitation, invitation_id: invitation_id) 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 case Repo.insert(user) do {:ok, user} -> diff --git a/lib/plausible_web/controllers/invitation_controller.ex b/lib/plausible_web/controllers/invitation_controller.ex index db1020ab2..51de2fefd 100644 --- a/lib/plausible_web/controllers/invitation_controller.ex +++ b/lib/plausible_web/controllers/invitation_controller.ex @@ -17,7 +17,9 @@ defmodule PlausibleWeb.InvitationController do multi = 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 Multi.new() end @@ -35,9 +37,10 @@ defmodule PlausibleWeb.InvitationController do |> Multi.delete(:invitation, invitation) case Repo.transaction(multi) do - {:ok, _} -> + {:ok, changes} -> + updated_user = Map.get(changes, :user, user) notify_invitation_accepted(invitation) - Plausible.Billing.SiteLocker.check_sites_for(user) + Plausible.Billing.SiteLocker.check_sites_for(updated_user) conn |> 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]) 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 invitation = Repo.get_by!(Invitation, invitation_id: invitation_id) diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 7a981aa72..786dc1fba 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -50,11 +50,14 @@ defmodule PlausibleWeb.SiteController do end def new(conn, _params) do - current_user = conn.assigns[:current_user] - site_count = Enum.count(Plausible.Sites.owned_by(current_user)) + current_user = conn.assigns[:current_user] |> Repo.preload(site_memberships: :site) + + 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) - is_at_limit = site_limit && site_count >= site_limit - is_first_site = site_count == 0 + is_at_limit = site_limit && owned_site_count >= site_limit + is_first_site = Enum.empty?(current_user.site_memberships) changeset = Plausible.Site.changeset(%Plausible.Site{}) diff --git a/lib/plausible_web/templates/site/index.html.eex b/lib/plausible_web/templates/site/index.html.eex index abb29e6d8..62d7e5f82 100644 --- a/lib/plausible_web/templates/site/index.html.eex +++ b/lib/plausible_web/templates/site/index.html.eex @@ -159,7 +159,15 @@ You've been invited to the analytics dashboard as Admin.

- 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 %> +

+ You will have to enter your card details immediately with no 30-day trial. + <% end %> + <%= if Plausible.Billing.on_trial?(@current_user) do %> +

+ Your 30-day free trial will end immediately and you will have to enter your card details to keep using Plausible. + <% end %>

diff --git a/lib/plausible_web/templates/site/new.html.eex b/lib/plausible_web/templates/site/new.html.eex index d3c6bca5a..260aec287 100644 --- a/lib/plausible_web/templates/site/new.html.eex +++ b/lib/plausible_web/templates/site/new.html.eex @@ -1,6 +1,7 @@
<%= 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 -> %>

Your website details

+ <%= if @is_at_limit do %>
@@ -22,6 +23,26 @@
<% end %> + + <%= if is_nil(@current_user.trial_expiry_date) do %> +
+
+
+ +
+
+
+

+ When you create your first site, your account will enter a 30 day free trial. +

+
+
+
+
+ <% end %> +
<%= label f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>

Just the naked domain or subdomain without 'www'

diff --git a/priv/repo/migrations/20210908081119_allow_trial_expiry_to_be_null.exs b/priv/repo/migrations/20210908081119_allow_trial_expiry_to_be_null.exs new file mode 100644 index 000000000..21e935703 --- /dev/null +++ b/priv/repo/migrations/20210908081119_allow_trial_expiry_to_be_null.exs @@ -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 diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index cb44b32b9..b6e3f3849 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -71,6 +71,116 @@ defmodule PlausibleWeb.AuthControllerTest do 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 setup [:create_user, :log_in] diff --git a/test/plausible_web/controllers/invitation_controller_test.exs b/test/plausible_web/controllers/invitation_controller_test.exs index 4e9671a5d..9c10e672f 100644 --- a/test/plausible_web/controllers/invitation_controller_test.exs +++ b/test/plausible_web/controllers/invitation_controller_test.exs @@ -101,6 +101,26 @@ defmodule PlausibleWeb.Site.InvitationControllerTest do assert Repo.reload!(site).locked 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 describe "POST /sites/invitations/:invitation_id/reject" do diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index 3a6dc0bd5..9794854d5 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -104,6 +104,19 @@ defmodule PlausibleWeb.SiteControllerTest do assert Repo.exists?(Plausible.Site, domain: "example.com") 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 post(conn, "/sites", %{ "site" => %{