File size: 7,584 Bytes
3e21b19 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | defmodule Plausible.Billing.Feature do
@moduledoc """
This module provides an interface for managing features, e.g. Revenue Goals,
Funnels and Custom Properties.
Feature modules have functions for toggling the feature on/off and checking
whether the feature is available for a site/user.
When defining new features, the following options are expected by the
`__using__` macro:
* `:name` - an atom representing the feature name in the plan JSON
file (see also Plausible.Billing.Plan).
* `:display_name` - human-readable display name of the feature
* `:toggle_display_name` - human-readable display name used in dashboard
toggle notifications. Defaults to `:display_name` if not set.
* `:toggle_field` - the field in the %Plausible.Site{} schema that toggles
the feature. If `nil` or not set, toggle/2 silently returns `:ok`
Functions defined by `__using__` can be overridden if needed.
"""
@doc """
Returns the atom representing the feature name in the plan JSON file.
"""
@callback name() :: atom()
@doc """
Returns the human-readable display name of the feature.
"""
@callback display_name() :: String.t()
@doc """
Returns the %Plausible.Site{} field that toggles the feature on and off.
"""
@callback toggle_field() :: atom()
@doc """
Toggles the feature on and off for a site. Returns
`{:error, :upgrade_required}` when toggling a feature the site owner does not
have access to.
"""
@callback toggle(Plausible.Site.t(), Plausible.Auth.User.t(), Keyword.t()) ::
:ok | {:error, :upgrade_required}
@doc """
Checks whether a feature is enabled or not. Returns false when the feature is
disabled or the user does not have access to it.
"""
@callback enabled?(Plausible.Site.t()) :: boolean()
@doc """
Returns whether the site explicitly opted out of the feature. This function
is different from enabled/1, because enabled/1 returns false when the site
owner does not have access to the feature.
"""
@callback opted_out?(Plausible.Site.t()) :: boolean()
@doc """
Checks whether the team or the team plan includes the given feature.
"""
@callback check_availability(Plausible.Teams.Team.t() | nil) ::
:ok | {:error, :upgrade_required} | {:error, :not_implemented}
@features [
Plausible.Billing.Feature.Props,
Plausible.Billing.Feature.SharedLinks,
Plausible.Billing.Feature.Funnels,
Plausible.Billing.Feature.Goals,
Plausible.Billing.Feature.RevenueGoals,
Plausible.Billing.Feature.SiteSegments,
Plausible.Billing.Feature.SiteAnnotations,
Plausible.Billing.Feature.SitesAPI,
Plausible.Billing.Feature.StatsAPI,
Plausible.Billing.Feature.SSO,
Plausible.Billing.Feature.ConsolidatedView
]
# Generate a union type for features
@type t() :: unquote(Enum.reduce(@features, &{:|, [], [&1, &2]}))
@doc """
Lists all available feature modules.
"""
def list() do
@features
end
@doc """
Lists all the feature short names, e.g. RevenueGoals
"""
defmacro list_short_names() do
@features
|> Enum.map(fn mod ->
Module.split(mod)
|> List.last()
|> String.to_atom()
end)
end
@doc false
defmacro __using__(opts \\ []) do
quote location: :keep do
@behaviour Plausible.Billing.Feature
alias Plausible.Billing.Quota
@impl true
def name, do: Keyword.get(unquote(opts), :name)
@impl true
def display_name, do: Keyword.get(unquote(opts), :display_name)
def toggle_display_name do
Keyword.get(unquote(opts), :toggle_display_name, display_name())
end
@impl true
def toggle_field, do: Keyword.get(unquote(opts), :toggle_field)
@impl true
def enabled?(%Plausible.Site{} = site) do
site = Plausible.Repo.preload(site, :team)
check_availability(site.team) == :ok && !opted_out?(site)
end
@impl true
def opted_out?(%Plausible.Site{} = site) do
if is_nil(toggle_field()), do: false, else: !Map.fetch!(site, toggle_field())
end
@impl true
def check_availability(team_or_nil) do
if __MODULE__ in Plausible.Teams.Billing.allowed_features_for(team_or_nil) do
:ok
else
{:error, :upgrade_required}
end
end
@impl true
def toggle(%Plausible.Site{} = site, %Plausible.Auth.User{} = user, opts \\ []) do
if toggle_field(), do: do_toggle(site, user, opts), else: :ok
end
defp do_toggle(%Plausible.Site{} = site, user, opts) do
override = Keyword.get(opts, :override)
toggle = if is_boolean(override), do: override, else: !Map.fetch!(site, toggle_field())
availability = if toggle, do: check_availability(site.team), else: :ok
case availability do
:ok ->
site
|> Ecto.Changeset.change(%{toggle_field() => toggle})
|> Plausible.Repo.update()
error ->
error
end
end
defoverridable check_availability: 1
end
end
end
defmodule Plausible.Billing.Feature.Funnels do
@moduledoc false
use Plausible.Billing.Feature,
name: :funnels,
display_name: "Funnels and user journeys",
toggle_display_name: "Funnels",
toggle_field: :funnels_enabled
end
defmodule Plausible.Billing.Feature.RevenueGoals do
@moduledoc false
use Plausible.Billing.Feature,
name: :revenue_goals,
display_name: "Revenue Goals"
end
defmodule Plausible.Billing.Feature.Goals do
@moduledoc false
use Plausible.Billing.Feature,
name: :goals,
display_name: "Goals",
toggle_field: :conversions_enabled
end
defmodule Plausible.Billing.Feature.Props do
@moduledoc false
use Plausible.Billing.Feature,
name: :props,
display_name: "Custom Properties",
toggle_field: :props_enabled
end
defmodule Plausible.Billing.Feature.SharedLinks do
@moduledoc false
use Plausible.Billing.Feature,
name: :shared_links,
display_name: "Shared Links"
end
defmodule Plausible.Billing.Feature.SiteSegments do
@moduledoc false
use Plausible.Billing.Feature,
name: :site_segments,
display_name: "Shared Segments"
end
defmodule Plausible.Billing.Feature.SiteAnnotations do
@moduledoc false
use Plausible.Billing.Feature,
name: :site_annotations,
display_name: "Shared Annotations"
end
defmodule Plausible.Billing.Feature.StatsAPI do
use Plausible
@moduledoc false
use Plausible.Billing.Feature,
name: :stats_api,
display_name: "Stats API"
end
defmodule Plausible.Billing.Feature.SitesAPI do
use Plausible
@moduledoc false
use Plausible.Billing.Feature,
name: :sites_api,
display_name: "Sites API"
end
defmodule Plausible.Billing.Feature.SSO do
use Plausible
@moduledoc false
use Plausible.Billing.Feature,
name: :sso,
display_name: "Single Sign-On"
end
defmodule Plausible.Billing.Feature.ConsolidatedView do
use Plausible
@moduledoc false
use Plausible.Billing.Feature,
name: :consolidated_view,
display_name: "Consolidated View"
end
defmodule Plausible.Billing.Feature.Teams do
@moduledoc """
Unlike other feature modules, this one only exists to make feature gating
settings views more convenient. Other than that, it's not even considered
a feature on its own. The real access to "Teams" is controlled by the
team member limit.
"""
def check_availability(team) do
if Plausible.Teams.Billing.solo?(team) do
{:error, :upgrade_required}
else
:ok
end
end
end
|