diff --git a/core/config/config.exs b/core/config/config.exs index c0655629..4a02d9ea 100644 --- a/core/config/config.exs +++ b/core/config/config.exs @@ -95,6 +95,7 @@ config :mime, :extensions, %{"json" => "application/vnd.api+json"} config :canary, :github_app_url, "https://github.com/apps/getcanary-dev/installations/new" config :ash, :missed_notifications, :ignore +config :ash, :policies, log_policy_breakdowns: :error config :floki, :html_parser, Floki.HTMLParser.Html5ever diff --git a/core/lib/canary/checks/filter_invite_access.ex b/core/lib/canary/checks/filter_invite_access.ex index 0cd3c144..95514155 100644 --- a/core/lib/canary/checks/filter_invite_access.ex +++ b/core/lib/canary/checks/filter_invite_access.ex @@ -7,25 +7,13 @@ defmodule Canary.Checks.Filter.InviteAccess do end @impl true - def filter( - _, - %Ash.Policy.Authorizer{ - resource: Canary.Accounts.Invite, - actor: %Canary.Accounts.Account{id: _} - }, - _opts - ) do + def filter(%Canary.Accounts.Account{id: _}, %Ash.Policy.Authorizer{} = _authorizer, _opts) do expr(account_id == ^actor(:id)) end - def filter( - _, - %Ash.Policy.Authorizer{ - resource: Canary.Accounts.Invite, - actor: %Canary.Accounts.User{email: _} - }, - _opts - ) do + def filter(%Canary.Accounts.User{email: _}, %Ash.Policy.Authorizer{} = _authorizer, _opts) do expr(email == ^actor(:email)) end + + def filter(_actor, _authorizer, _opts), do: false end diff --git a/core/lib/canary/checks/membership_source_create.ex b/core/lib/canary/checks/membership_source_create.ex index 27c5add4..f9c7eeb1 100644 --- a/core/lib/canary/checks/membership_source_create.ex +++ b/core/lib/canary/checks/membership_source_create.ex @@ -9,20 +9,15 @@ defmodule Canary.Checks.Membership.SourceCreate do %Canary.Accounts.Account{} = account, %Ash.Policy.Authorizer{ resource: Canary.Sources.Source, - changeset: %Ash.Changeset{relationships: %{project: [{[%{id: id}], _}]}} = changeset + changeset: %Ash.Changeset{relationships: %{project: [{[%{id: id}], _}]}} }, _opts ) do with {:ok, %{billing: billing}} <- Ash.load(account, billing: [:membership]), {:ok, %{num_sources: num_sources}} <- Ash.get(Canary.Accounts.Project, id, load: [:num_sources]) do - %Ash.Union{type: source_type} = Ash.Changeset.get_attribute(changeset, :config) - cond do - billing.membership.tier == :free and source_type == :webpage and num_sources < 1 -> - {:ok, true} - - billing.membership.tier == :starter and num_sources < 4 -> + num_sources < Canary.Membership.max_sources(billing.membership.tier) -> {:ok, true} true -> diff --git a/core/lib/canary/checks/membership_team_invite.ex b/core/lib/canary/checks/membership_team_invite.ex index 92d09a8d..b60479ba 100644 --- a/core/lib/canary/checks/membership_team_invite.ex +++ b/core/lib/canary/checks/membership_team_invite.ex @@ -24,18 +24,16 @@ defmodule Canary.Checks.Membership.TeamInvite do account != account_id -> {:ok, false} - billing.membership.tier == :free -> - {:ok, false} - - billing.membership.tier == :starter and num_invites + num_members < 4 -> + num_invites + num_members <= Canary.Membership.max_members(billing.membership.tier) -> {:ok, true} true -> {:ok, false} end + else + _ -> + {:ok, false} end - - {:ok, true} end def match?(_, _, _), do: false diff --git a/core/lib/canary/membership.ex b/core/lib/canary/membership.ex index 4ab6538e..0b002286 100644 --- a/core/lib/canary/membership.ex +++ b/core/lib/canary/membership.ex @@ -6,7 +6,6 @@ defmodule Canary.Membership do :free -> false :starter -> true :admin -> true - _ -> false end end @@ -17,72 +16,90 @@ defmodule Canary.Membership do :free -> false :starter -> true :admin -> true - _ -> false end end - def max_sources(%Canary.Accounts.Account{} = account) do + def max_projects(:free), do: 1 + def max_projects(:starter), do: 3 + def max_projects(:admin), do: 9999 + + def max_projects(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 1 - :starter -> 3 - :admin -> 9999 - _ -> 0 + :free -> Canary.Membership.max_projects(:free) + :starter -> Canary.Membership.max_projects(:starter) + :admin -> Canary.Membership.max_projects(:admin) end end - def max_projects(%Canary.Accounts.Account{} = account) do + def max_sources(:free), do: 3 + def max_sources(:starter), do: 9 + def max_sources(:admin), do: 9999 + + def max_sources(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 1 - :starter -> 3 - :admin -> 9999 - _ -> 0 + :free -> Canary.Membership.max_sources(:free) + :starter -> Canary.Membership.max_sources(:starter) + :admin -> Canary.Membership.max_sources(:admin) end end + def max_members(:free), do: 1 + def max_members(:starter), do: 3 + def max_members(:admin), do: 9999 + def max_members(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 1 - :starter -> 3 - :admin -> 9999 - _ -> 0 + :free -> Canary.Membership.max_members(:free) + :starter -> Canary.Membership.max_members(:starter) + :admin -> Canary.Membership.max_members(:admin) end end + def max_searches(:free), do: 1000 * 1000 + def max_searches(:starter), do: 10 * 1000 * 1000 + def max_searches(:admin), do: 1000 * 1000 * 1000 + def max_searches(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 30 * 1000 - :starter -> 1000 * 1000 - :admin -> 1000 * 1000 - _ -> 0 + :free -> Canary.Membership.max_searches(:free) + :starter -> Canary.Membership.max_searches(:starter) + :admin -> Canary.Membership.max_searches(:admin) end end + def max_asks(:free), do: 0 + def max_asks(:starter), do: 3 * 1000 + def max_asks(:admin), do: 1000 * 1000 + def max_asks(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 100 - :starter -> 1000 - :admin -> 1000 * 1000 - _ -> 0 + :free -> Canary.Membership.max_asks(:free) + :starter -> Canary.Membership.max_asks(:starter) + :admin -> Canary.Membership.max_asks(:admin) end end + def refetch_interval_hours(:free), do: 24 * 4 + def refetch_interval_hours(:starter), do: 24 * 1 + def refetch_interval_hours(:admin), do: 24 * 30 * 12 * 10 + def refetch_interval_hours(%Canary.Accounts.Account{} = account) do account = ensure_membership(account) case account.billing.membership.tier do - :free -> 24 * 3 - :starter -> 24 * 1 - _ -> 24 * 30 * 12 * 10 + :free -> Canary.Membership.refetch_interval_hours(:free) + :starter -> Canary.Membership.refetch_interval_hours(:starter) + :admin -> Canary.Membership.refetch_interval_hours(:admin) end end @@ -92,7 +109,12 @@ defmodule Canary.Membership do account rescue _ -> - account |> Ash.load!(billing: [:membership]) + billing_query = + Canary.Accounts.Billing + |> Ash.Query.select([:id]) + |> Ash.Query.load(:membership) + + account |> Ash.load!(billing: billing_query) end end end diff --git a/core/lib/canary_web/live/billing_live/plans.ex b/core/lib/canary_web/live/billing_live/plans.ex index ae0e9004..e755cf95 100644 --- a/core/lib/canary_web/live/billing_live/plans.ex +++ b/core/lib/canary_web/live/billing_live/plans.ex @@ -5,6 +5,9 @@ defmodule CanaryWeb.BillingLive.Plans do def render(assigns) do ~H"""
+
+ For more information, please refer to our pricing page. +
@@ -14,8 +17,9 @@ defmodule CanaryWeb.BillingLive.Plans do value <- [ "Name", "Price", + "Projects", "Users", - "Source", + "Sources", "Reindex", "Search", "Ask AI", @@ -36,13 +40,14 @@ defmodule CanaryWeb.BillingLive.Plans do value <- [ "Free", "$0 / mo", - "<= 1", - "<= 1, Webpage only", - "Every 72 hours", - (1000 * 1000) - |> Integer.to_string() - |> String.pad_leading(3, "0"), - "100", + "<= #{Canary.Membership.max_projects(:free)}", + "<= #{Canary.Membership.max_members(:free)}", + "<= #{Canary.Membership.max_sources(:free)}", + "Every #{Canary.Membership.refetch_interval_hours(:free)} hours", + Canary.Membership.max_searches(:free) + |> Number.Delimit.number_to_delimited(precision: 0), + Canary.Membership.max_asks(:free) + |> Number.Delimit.number_to_delimited(precision: 0), "X", nil ] @@ -61,14 +66,15 @@ defmodule CanaryWeb.BillingLive.Plans do :for={ value <- [ "Starter", - "$59 / mo", - "<= 3", - "<= 3, All types", - "Every 24 hours", - (5000 * 1000) - |> Integer.to_string() - |> String.pad_leading(3, "0"), - 1000, + "$79 / mo", + "<= #{Canary.Membership.max_projects(:starter)}", + "<= #{Canary.Membership.max_members(:starter)}", + "<= #{Canary.Membership.max_sources(:starter)}", + "Every #{Canary.Membership.refetch_interval_hours(:starter)} hours", + Canary.Membership.max_searches(:starter) + |> Number.Delimit.number_to_delimited(precision: 0), + Canary.Membership.max_asks(:starter) + |> Number.Delimit.number_to_delimited(precision: 0), "O", :action ] diff --git a/core/lib/canary_web/live/billing_live/stats.ex b/core/lib/canary_web/live/billing_live/stats.ex index 90a2324c..a2550de2 100644 --- a/core/lib/canary_web/live/billing_live/stats.ex +++ b/core/lib/canary_web/live/billing_live/stats.ex @@ -9,74 +9,83 @@ defmodule CanaryWeb.BillingLive.Stats do "col-span-4 grid grid-cols-1 divide-y divide-gray-200 overflow-hidden rounded-lg border bg-white shadow", "md:grid-cols-2 md:divide-x md:divide-y-0" ]}> + <.render_plan plan_exceeded={@plan_exceeded} current_account={@current_account} /> <%= for metric <- @metrics do %> - <%= if is_nil(metric) do %> -
-
Current Plan
-
- - <%= case @current_account.billing.membership.tier do %> - <% :free -> %> - Free - <% :starter -> %> - Starter - <% :admin -> %> - Admin - <% end %> - - - Trial ends on <%= Calendar.strftime( - @current_account.billing.membership.grant_end, - "%B %d, %Y" - ) %> - - - Trial ends on <%= Calendar.strftime( - @current_account.billing.membership.trial_end, - "%B %d, %Y" - ) %> - - - You have exceeded your limit. Please upgrade your plan. - -
-
- <% else %> -
-
<%= metric.title %>
-
-
-
- metric.total && "text-red-600" - ]}> - <%= metric.current %> - - metric.total && "text-red-600" - ]}> - of <%= metric.total %> - -
-
-
-
-
-
-
-
- <% end %> + <.render_metric metric={metric} current_account={@current_account} /> <% end %> """ end + defp render_plan(assigns) do + ~H""" +
+
Current Plan
+
+ + <%= case @current_account.billing.membership.tier do %> + <% :free -> %> + Free + <% :starter -> %> + Starter + <% :admin -> %> + Admin + <% end %> + + + Trial ends on <%= Calendar.strftime( + @current_account.billing.membership.grant_end, + "%B %d, %Y" + ) %> + + + Trial ends on <%= Calendar.strftime( + @current_account.billing.membership.trial_end, + "%B %d, %Y" + ) %> + + + You have exceeded your limit. Please upgrade your plan. + +
+
+ """ + end + + defp render_metric(assigns) do + ~H""" +
+
<%= @metric.title %>
+
+
+
+ @metric.total && "text-red-600" + ]}> + <%= Number.Delimit.number_to_delimited(@metric.current, precision: 0) %> + + @metric.total && "text-red-600" + ]}> + of <%= Number.Delimit.number_to_delimited(@metric.total, precision: 0) %> + +
+
+
+
+
+
+
+
+ """ + end + @impl true def update(assigns, socket) do current_account = assigns.current_account |> Ash.load!([:users, :projects]) @@ -97,7 +106,6 @@ defmodule CanaryWeb.BillingLive.Stats do usage |> Enum.find(%{"type" => "ask", "sum" => 0}, &(&1["type"] == "ask")) metrics = [ - nil, %{ title: "Total Projects", current: length(current_account.projects), diff --git a/core/lib/canary_web/live/members_live/index.ex b/core/lib/canary_web/live/members_live/index.ex index 7d88b6ab..3f3ba6a4 100644 --- a/core/lib/canary_web/live/members_live/index.ex +++ b/core/lib/canary_web/live/members_live/index.ex @@ -5,9 +5,14 @@ defmodule CanaryWeb.MembersLive.Index do def render(assigns) do ~H"""
-
-

Members

- <.button phx-click={show_modal("invite-member-modal")} is_primary>Invite +
+
+

Members

+ <.button phx-click={show_modal("invite-member-modal")} is_primary>Invite +
+

+ Members can access all projects, and can invite other members. +

<.modal id="invite-member-modal" on_cancel={JS.navigate(~p"/members")}> diff --git a/core/mix.exs b/core/mix.exs index d7afd2ba..10ffe61a 100644 --- a/core/mix.exs +++ b/core/mix.exs @@ -112,7 +112,8 @@ defmodule Canary.MixProject do {:recon_ex, github: "tatsuya6502/recon_ex", ref: "0ce4c5da777937a5bb57d3e68b9afcb9877c1c3b"}, {:live_toast, "~> 0.6.4"}, - {:flow, "~> 1.0"} + {:flow, "~> 1.0"}, + {:number, "~> 1.0.1"} ] ++ deps_eval() end diff --git a/core/mix.lock b/core/mix.lock index 26bffda3..cad7f2bb 100644 --- a/core/mix.lock +++ b/core/mix.lock @@ -81,6 +81,7 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nodejs": {:hex, :nodejs, "2.0.0", "9a00d00eabf84ba7a04269de46863e0f87bdf6bc488d5a20972b38ade9012764", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "3a03df7dbfba435223b4534fbf276db8be5287fbf83c828f2749bf1ffe73e930"}, "nostrum": {:hex, :nostrum, "0.8.0", "36f5a08e99c3df3020523be9e1c650ad926a63becc5318562abfe782d586e078", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "ce6861391ff346089d32a243fa71c0cb8bff79ab86ad53e8bf72808267899aee"}, + "number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"}, "nx": {:hex, :nx, "0.7.3", "51ff45d9f9ff58b616f4221fa54ccddda98f30319bb8caaf86695234a469017a", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ff29af84f08db9bda66b8ef7ce92ab583ab4f983629fe00b479f1e5c7c705a6"}, "nx_image": {:hex, :nx_image, "0.1.2", "0c6e3453c1dc30fc80c723a54861204304cebc8a89ed3b806b972c73ee5d119d", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "9161863c42405ddccb6dbbbeae078ad23e30201509cc804b3b3a7c9e98764b81"}, "nx_signal": {:hex, :nx_signal, "0.2.0", "e1ca0318877b17c81ce8906329f5125f1e2361e4c4235a5baac8a95ee88ea98e", [:mix], [{:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "7247e5e18a177a59c4cb5355952900c62fdeadeb2bad02a9a34237b68744e2bb"}, diff --git a/js/apps/docs/contents/docs/cloud/platform/pricing.md b/js/apps/docs/contents/docs/cloud/platform/pricing.md index 0c49454e..5edb041a 100644 --- a/js/apps/docs/contents/docs/cloud/platform/pricing.md +++ b/js/apps/docs/contents/docs/cloud/platform/pricing.md @@ -9,17 +9,16 @@ We offer hosted service at [cloud.getcanary.dev](https://cloud.getcanary.dev). ## Free (`$0` / month) -- Up to `1` project, `1` user, and `1` source -- `Webpage` source only, limited to `< 300 pages` -- Index update `twice a week` +- Up to `1` project, `1` user, and `3` source. +- `Webpage`, `GitHub issue`, and `GitHub discussion` source only. +- Index update every `96 hours`. - Blazing fast `Hybrid`(keyword-based + semantic) `search` -## Starter (`$59` / month) +## Starter (`$79` / month) -- Up to `1` project, `3` users, and `3` sources -- `Any source` we support. (currently `Webpage`, `GitHub issue`, and `GitHub discussion` with some limitations[^1]) -- `Daily` index update +- Up to `3` project, `3` users, and `9` sources +- `Any source` we support. +- Index update every `24 hours`. - Blazing fast `Hybrid`(keyword-based + semantic) `search` +- `Ask AI` support, Up to `1000` questions per month. - `Search analytics` (currently we have `query-volume` and `query-breakdown`, but more to come) - -[^1]: We might ask you to install our `Github App` in the future, to avoid rate limiting.