Staging subscriptions (#5349)
* sandbox_plans.json -> sandbox_plans_v4.json * add mix task and generate sandbox plans * manually add sandbox_legacy_plans.json * make all staging plans consistent with prod * slight code style improvement * add kb link
This commit is contained in:
parent
b942081f30
commit
1de37a125c
|
|
@ -0,0 +1,203 @@
|
|||
defmodule Mix.Tasks.CreatePaddleSandboxPlans do
|
||||
@moduledoc """
|
||||
Utility for creating Sandbox plans that are used on staging. The `filename`
|
||||
argument should be the name of the JSON file containing the production plans.
|
||||
E.g.: `plans_v4.json`.
|
||||
|
||||
Unfortunately, there's no API in Paddle that would allow "bulk creating"
|
||||
plans - it has to be done through the UI. As a hack though, we can automate
|
||||
the process by copying the curl request with the help of browser devtools.
|
||||
|
||||
Therefore, this Mix.Task **does not work out of the box** and the actual curl
|
||||
command that it executes must be replaced by the developer. Here's how:
|
||||
|
||||
0) Obtain access to the Sandbox Paddle account and log in
|
||||
1) Navigate to https://sandbox-vendors.paddle.com/subscriptions/plans
|
||||
2) Click the "+ New Plan" button to open the form
|
||||
3) Open browser devtools, fill in the required fields and submit the form
|
||||
4) Find the POST request from the "Network" tab and copy it as cURL
|
||||
5) Come back here and paste it into the `create_paddle_plan` function
|
||||
6) Replace the params within the string with the real params (these should
|
||||
be available in the function already)
|
||||
|
||||
Once the plans are created successfully, the task will also fetch the IDs
|
||||
(i.e. paddle_plan_id's) and write the `sandbox_plans_v*.json` file (which is
|
||||
basically the same set of production plans but with `monthly_product_id` and
|
||||
`yearly_product_id` replaced with the sandbox ones).
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
@requirements ["app.config"]
|
||||
|
||||
def run([filename]) do
|
||||
{:ok, _} = Application.ensure_all_started(:telemetry)
|
||||
Finch.start_link(name: MyFinch)
|
||||
|
||||
prod_plans =
|
||||
Application.app_dir(:plausible, ["priv", filename])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
|
||||
to_be_created =
|
||||
prod_plans
|
||||
|> put_prices()
|
||||
|> Enum.flat_map(fn priced_plan ->
|
||||
[
|
||||
create_paddle_plan_attrs(priced_plan, "monthly"),
|
||||
create_paddle_plan_attrs(priced_plan, "yearly")
|
||||
]
|
||||
end)
|
||||
|
||||
IO.puts("Fetching all sandbox plans before we get started...")
|
||||
paddle_plans_before = fetch_all_sandbox_plans()
|
||||
|
||||
created =
|
||||
Enum.filter(to_be_created, fn attrs ->
|
||||
if Enum.any?(paddle_plans_before, &(&1["name"] == attrs.name)) do
|
||||
IO.puts("⚠️ The plan #{attrs.name} already exists in Sandbox Paddle")
|
||||
false
|
||||
else
|
||||
create_paddle_plan(attrs)
|
||||
true
|
||||
end
|
||||
end)
|
||||
|
||||
paddle_plans_after =
|
||||
if created != [] do
|
||||
IO.puts("⏳ waiting 3s before fetching the newly created plans...")
|
||||
Process.sleep(3000)
|
||||
IO.puts("Fetching all sandbox plans after creation...")
|
||||
fetch_all_sandbox_plans()
|
||||
else
|
||||
IO.puts("All plans have been created already.")
|
||||
paddle_plans_before
|
||||
end
|
||||
|
||||
file_path_to_write = Path.join("priv", "sandbox_" <> filename)
|
||||
|
||||
create_local_sandbox_plans_json_file(prod_plans, file_path_to_write, paddle_plans_after)
|
||||
|
||||
IO.puts("✅ All done! Wrote #{length(prod_plans)} new plans into #{file_path_to_write}!")
|
||||
end
|
||||
|
||||
defp create_paddle_plan(%{name: name, price: price, type: type, interval_index: interval_index}) do
|
||||
your_unique_token = "abc"
|
||||
|
||||
# Replace this curl command. You might be able to reuse
|
||||
# the request body after replacing your unique token.
|
||||
curl_command = """
|
||||
... REPLACE ME
|
||||
--data-raw '_token=#{your_unique_token}&plan-id=&default-curr=USD&tmpicon=false&name=#{name}&checkout_custom_message=&taxable_type=standard&interval=#{interval_index}&period=1&type=#{type}&trial_length=&price_USD=#{price}&active_EUR=on&price_EUR=#{price}&active_GBP=on&price_GBP=#{price}'
|
||||
"""
|
||||
|
||||
case curl_quietly(curl_command) do
|
||||
:ok ->
|
||||
IO.puts("✅ Created #{name}")
|
||||
|
||||
{:error, reason} ->
|
||||
IO.puts("❌ Halting. The plan #{name} could not be created. Error: #{reason}")
|
||||
System.halt(1)
|
||||
end
|
||||
end
|
||||
|
||||
@paddle_interval_indexes %{"monthly" => 2, "yearly" => 5}
|
||||
|
||||
defp create_paddle_plan_attrs(plan_with_price, type) do
|
||||
%{
|
||||
name: plan_name(plan_with_price, type),
|
||||
price: plan_with_price["#{type}_price"],
|
||||
type: type,
|
||||
interval_index: @paddle_interval_indexes[type]
|
||||
}
|
||||
end
|
||||
|
||||
defp fetch_all_sandbox_plans() do
|
||||
paddle_config = Application.get_env(:plausible, :paddle)
|
||||
|
||||
url = "https://sandbox-vendors.paddle.com/api/2.0/subscription/plans"
|
||||
|
||||
body =
|
||||
JSON.encode!(%{
|
||||
vendor_id: paddle_config[:vendor_id],
|
||||
vendor_auth_code: paddle_config[:vendor_auth_code]
|
||||
})
|
||||
|
||||
headers = [
|
||||
{"Content-type", "application/json"},
|
||||
{"Accept", "application/json"}
|
||||
]
|
||||
|
||||
request = Finch.build(:post, url, headers, body)
|
||||
|
||||
with {:ok, response} <- Finch.request(request, MyFinch),
|
||||
{:ok, %{"success" => true, "response" => plans} = body} <- JSON.decode(response.body) do
|
||||
IO.puts("✅ Successfully fetched #{body["count"]}/#{body["total"]} sandbox plans")
|
||||
plans
|
||||
else
|
||||
error ->
|
||||
IO.puts("❌ Failed to fetch plans from Paddle - #{inspect(error)}")
|
||||
System.halt(1)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_local_sandbox_plans_json_file(prod_plans, filepath, paddle_plans) do
|
||||
sandbox_plans =
|
||||
prod_plans
|
||||
|> Enum.map(fn prod_plan ->
|
||||
monthly_plan_name = plan_name(prod_plan, "monthly")
|
||||
yearly_plan_name = plan_name(prod_plan, "yearly")
|
||||
|
||||
%{"id" => sandbox_monthly_product_id} =
|
||||
Enum.find(paddle_plans, &(&1["name"] == monthly_plan_name))
|
||||
|
||||
%{"id" => sandbox_yearly_product_id} =
|
||||
Enum.find(paddle_plans, &(&1["name"] == yearly_plan_name))
|
||||
|
||||
Map.merge(prod_plan, %{
|
||||
"monthly_product_id" => to_string(sandbox_monthly_product_id),
|
||||
"yearly_product_id" => to_string(sandbox_yearly_product_id)
|
||||
})
|
||||
end)
|
||||
|
||||
File.write!(filepath, JSON.encode!(sandbox_plans))
|
||||
end
|
||||
|
||||
defp put_prices(plans) do
|
||||
prices =
|
||||
Application.app_dir(:plausible, ["priv", "plan_prices.json"])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
|
||||
plans
|
||||
|> Enum.map(fn plan ->
|
||||
Map.merge(plan, %{
|
||||
"monthly_price" => prices[plan["monthly_product_id"]],
|
||||
"yearly_price" => prices[plan["yearly_product_id"]]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
defp plan_name(plan, type) do
|
||||
generation = "v#{plan["generation"]}"
|
||||
kind = plan["kind"]
|
||||
volume = plan["monthly_pageview_limit"] |> PlausibleWeb.StatsView.large_number_format()
|
||||
|
||||
[generation, type, kind, volume] |> Enum.join("_")
|
||||
end
|
||||
|
||||
defp curl_quietly(cmd) do
|
||||
cmd = String.replace(cmd, "curl", ~s|curl -s -o /dev/null -w "%{http_code}"|)
|
||||
|
||||
case System.cmd("sh", ["-c", cmd], stderr_to_stdout: true) do
|
||||
{"302", 0} ->
|
||||
:ok
|
||||
|
||||
{http_status, 0} ->
|
||||
{:error, "unexpected HTTP response status (#{http_status}). Expected 302."}
|
||||
|
||||
{_, exit_code} ->
|
||||
{:error, "curl command exited with exit code #{exit_code}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,27 +4,34 @@ defmodule Plausible.Billing.Plans do
|
|||
alias Plausible.Billing.{Subscription, Plan, EnterprisePlan}
|
||||
alias Plausible.Teams
|
||||
|
||||
for f <- [
|
||||
:legacy_plans,
|
||||
:plans_v1,
|
||||
:plans_v2,
|
||||
:plans_v3,
|
||||
:plans_v4,
|
||||
:sandbox_plans
|
||||
] do
|
||||
path = Application.app_dir(:plausible, ["priv", "#{f}.json"])
|
||||
@generations [:legacy_plans, :plans_v1, :plans_v2, :plans_v3, :plans_v4]
|
||||
|
||||
for group <- Enum.flat_map(@generations, &[&1, :"sandbox_#{&1}"]) do
|
||||
path = Application.app_dir(:plausible, ["priv", "#{group}.json"])
|
||||
|
||||
plans_list =
|
||||
for attrs <- path |> File.read!() |> Jason.decode!() do
|
||||
%Plan{} |> Plan.changeset(attrs) |> Ecto.Changeset.apply_action!(nil)
|
||||
end
|
||||
|
||||
Module.put_attribute(__MODULE__, f, plans_list)
|
||||
Module.put_attribute(__MODULE__, group, plans_list)
|
||||
|
||||
# https://hexdocs.pm/elixir/1.15/Module.html#module-external_resource
|
||||
Module.put_attribute(__MODULE__, :external_resource, path)
|
||||
end
|
||||
|
||||
# Generate functions returning a specific generation of plans depending on
|
||||
# the app environment
|
||||
for fn_name <- @generations do
|
||||
defp unquote(fn_name)() do
|
||||
if Application.get_env(:plausible, :environment) == "staging" do
|
||||
unquote(Macro.escape(Module.get_attribute(__MODULE__, :"sandbox_#{fn_name}")))
|
||||
else
|
||||
unquote(Macro.escape(Module.get_attribute(__MODULE__, fn_name)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec growth_plans_for(Subscription.t()) :: [Plan.t()]
|
||||
@doc """
|
||||
Returns a list of growth plans available for the subscription to choose.
|
||||
|
|
@ -36,14 +43,13 @@ defmodule Plausible.Billing.Plans do
|
|||
owned_plan = get_regular_plan(subscription)
|
||||
|
||||
cond do
|
||||
Application.get_env(:plausible, :environment) == "staging" -> @sandbox_plans
|
||||
is_nil(owned_plan) -> @plans_v4
|
||||
subscription && Subscriptions.expired?(subscription) -> @plans_v4
|
||||
owned_plan.kind == :business -> @plans_v4
|
||||
owned_plan.generation == 1 -> @plans_v1 |> drop_high_plans(owned_plan)
|
||||
owned_plan.generation == 2 -> @plans_v2 |> drop_high_plans(owned_plan)
|
||||
owned_plan.generation == 3 -> @plans_v3
|
||||
owned_plan.generation == 4 -> @plans_v4
|
||||
is_nil(owned_plan) -> plans_v4()
|
||||
subscription && Subscriptions.expired?(subscription) -> plans_v4()
|
||||
owned_plan.kind == :business -> plans_v4()
|
||||
owned_plan.generation == 1 -> plans_v1() |> drop_high_plans(owned_plan)
|
||||
owned_plan.generation == 2 -> plans_v2() |> drop_high_plans(owned_plan)
|
||||
owned_plan.generation == 3 -> plans_v3()
|
||||
owned_plan.generation == 4 -> plans_v4()
|
||||
end
|
||||
|> Enum.filter(&(&1.kind == :growth))
|
||||
end
|
||||
|
|
@ -52,10 +58,9 @@ defmodule Plausible.Billing.Plans do
|
|||
owned_plan = get_regular_plan(subscription)
|
||||
|
||||
cond do
|
||||
Application.get_env(:plausible, :environment) == "staging" -> @sandbox_plans
|
||||
subscription && Subscriptions.expired?(subscription) -> @plans_v4
|
||||
owned_plan && owned_plan.generation < 4 -> @plans_v3
|
||||
true -> @plans_v4
|
||||
subscription && Subscriptions.expired?(subscription) -> plans_v4()
|
||||
owned_plan && owned_plan.generation < 4 -> plans_v3()
|
||||
true -> plans_v4()
|
||||
end
|
||||
|> Enum.filter(&(&1.kind == :business))
|
||||
end
|
||||
|
|
@ -223,14 +228,6 @@ defmodule Plausible.Billing.Plans do
|
|||
end
|
||||
|
||||
def all() do
|
||||
@legacy_plans ++ @plans_v1 ++ @plans_v2 ++ @plans_v3 ++ @plans_v4 ++ sandbox_plans()
|
||||
end
|
||||
|
||||
defp sandbox_plans() do
|
||||
if Application.get_env(:plausible, :environment) == "staging" do
|
||||
@sandbox_plans
|
||||
else
|
||||
[]
|
||||
end
|
||||
legacy_plans() ++ plans_v1() ++ plans_v2() ++ plans_v3() ++ plans_v4()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"kind":"growth",
|
||||
"generation": 1,
|
||||
"monthly_pageview_limit":1000000,
|
||||
"yearly_product_id":"78110",
|
||||
"monthly_product_id":"78111",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
"generation": 1,
|
||||
"monthly_pageview_limit":150000000,
|
||||
"yearly_product_id":"78112",
|
||||
"monthly_product_id":null,
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","stats_api"]
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
[
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000,
|
||||
"monthly_product_id": "78008",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78009"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 100000,
|
||||
"monthly_product_id": "78010",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78011"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 200000,
|
||||
"monthly_product_id": "78012",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78013"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 500000,
|
||||
"monthly_product_id": "78014",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78015"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 1000000,
|
||||
"monthly_product_id": "78016",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78017"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 2000000,
|
||||
"monthly_product_id": "78018",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78019"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 5000000,
|
||||
"monthly_product_id": "78020",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78021"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000000,
|
||||
"monthly_product_id": "78022",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78023"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 20000000,
|
||||
"monthly_product_id": "78024",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78025"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 1,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 50000000,
|
||||
"monthly_product_id": "78026",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78027"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
[
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000,
|
||||
"monthly_product_id": "78053",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78054"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 100000,
|
||||
"monthly_product_id": "78055",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78056"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 200000,
|
||||
"monthly_product_id": "78057",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78058"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 500000,
|
||||
"monthly_product_id": "78059",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78060"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 1000000,
|
||||
"monthly_product_id": "78061",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78062"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 2000000,
|
||||
"monthly_product_id": "78063",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78064"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 5000000,
|
||||
"monthly_product_id": "78065",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78066"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000000,
|
||||
"monthly_product_id": "78067",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78068"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 20000000,
|
||||
"monthly_product_id": "78069",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78070"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 2,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 50000000,
|
||||
"monthly_product_id": "78071",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78072"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
[
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000,
|
||||
"monthly_product_id": "78073",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78074"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 100000,
|
||||
"monthly_product_id": "78075",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78076"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 200000,
|
||||
"monthly_product_id": "78077",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78078"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 500000,
|
||||
"monthly_product_id": "78079",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78080"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 1000000,
|
||||
"monthly_product_id": "78081",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78082"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 2000000,
|
||||
"monthly_product_id": "78083",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78084"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 5000000,
|
||||
"monthly_product_id": "78085",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78086"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"stats_api"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "growth",
|
||||
"monthly_pageview_limit": 10000000,
|
||||
"monthly_product_id": "78087",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78088"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 10000,
|
||||
"monthly_product_id": "78089",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78090"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 100000,
|
||||
"monthly_product_id": "78091",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78092"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 200000,
|
||||
"monthly_product_id": "78093",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78094"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 500000,
|
||||
"monthly_product_id": "78095",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78096"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 1000000,
|
||||
"monthly_product_id": "78097",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78098"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 2000000,
|
||||
"monthly_product_id": "78099",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78100"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 5000000,
|
||||
"monthly_product_id": "78101",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78102"
|
||||
},
|
||||
{
|
||||
"features": [
|
||||
"goals",
|
||||
"props",
|
||||
"revenue_goals",
|
||||
"funnels",
|
||||
"stats_api",
|
||||
"site_segments"
|
||||
],
|
||||
"generation": 3,
|
||||
"kind": "business",
|
||||
"monthly_pageview_limit": 10000000,
|
||||
"monthly_product_id": "78107",
|
||||
"site_limit": 50,
|
||||
"team_member_limit": "unlimited",
|
||||
"yearly_product_id": "78109"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,33 +1,36 @@
|
|||
[
|
||||
{
|
||||
"kind":"growth",
|
||||
"generation":1,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":10000,
|
||||
"monthly_product_id":"63842",
|
||||
"yearly_product_id":"63859",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","stats_api"]
|
||||
"features":["goals"],
|
||||
"data_retention_in_years": 3
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
"generation":2,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":100000,
|
||||
"monthly_product_id":"63843",
|
||||
"yearly_product_id":"63860",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","stats_api"]
|
||||
"features":["goals"],
|
||||
"data_retention_in_years": 3
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
"generation":3,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":200000,
|
||||
"monthly_product_id":"63844",
|
||||
"yearly_product_id":"63861",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","stats_api"]
|
||||
"features":["goals"],
|
||||
"data_retention_in_years": 3
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
|
|
@ -86,33 +89,36 @@
|
|||
},
|
||||
{
|
||||
"kind":"business",
|
||||
"generation":3,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":10000,
|
||||
"monthly_product_id":"63850",
|
||||
"yearly_product_id":"63867",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"]
|
||||
"team_member_limit":10,
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"],
|
||||
"data_retention_in_years": 5
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
"generation":3,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":100000,
|
||||
"monthly_product_id":"63851",
|
||||
"yearly_product_id":"63868",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"]
|
||||
"team_member_limit":10,
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"],
|
||||
"data_retention_in_years": 5
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
"generation":3,
|
||||
"generation":4,
|
||||
"monthly_pageview_limit":200000,
|
||||
"monthly_product_id":"63852",
|
||||
"yearly_product_id":"63869",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"]
|
||||
"team_member_limit":10,
|
||||
"features":["goals","props","revenue_goals","funnels","stats_api","site_segments"],
|
||||
"data_retention_in_years": 5
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
|
|
@ -16,7 +16,11 @@
|
|||
|
||||
<br />
|
||||
This local implementation skips steps 1-3, redirecting you first to <b><code>/billing/upgrade-success</code></b>, and after a short artificial delay, to <b><code>/settings</code></b>, with the subscription created.
|
||||
<br /><br /> The real Paddle integration can be tested in "Sandbox" mode on staging.
|
||||
<br /><br />
|
||||
Read more about dev and staging subscriptions in the <a
|
||||
class="text-indigo-600"
|
||||
href="https://kb.plausible.io/engineering/how-to/dev-and-staging-subscriptions"
|
||||
>knowledge base</a>.
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-10">
|
||||
|
|
|
|||
Loading…
Reference in New Issue