mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
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:
parent
c8a1b5c73c
commit
700a65c98a
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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} ->
|
||||||
|
@ -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)
|
||||||
|
@ -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{})
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
@ -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]
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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" => %{
|
||||||
|
Loading…
Reference in New Issue
Block a user