A prebuilt subscription management UI for Elixir and Phoenix, built on Bling.
Scaffold a new phoenix app with authentication:
mix phx.new my_app
cd my_app
mix phx.gen.auth Accounts User users
Download the latest release and extract the contents into a folder called bankroll
in your project folder:
# in ./my_app
mkdir bankroll
tar -xvf bankroll-x.x.x.tar.gz -C ./bankroll
Add the dependencies to your mix file:
defp deps do
[
{:stripity_stripe, "~> 2.17"},
{:bling, "~> 0.4.0"},
{:bankroll, path: "./bankroll"}
]
end
Install the dependencies:
mix deps.get
Note: If your mix.exs file shows an error in your editor after installing the dependencies, restart your elixir language server.
Follow the steps to install the Bling module:
Run the bankroll install command to create the required files and copy over assets:
mix bankroll.install
Open up your router file and add the bankroll route right after the one installed with Bling:
defmodule MyAppWeb.Router do
import Bankroll.Router
# ... your routes
scope "/" do
pipe_through [:browser, :require_authenticated_user]
bling_routes()
bankroll_routes()
end
end
Now we need to configure the modules and plans in config/config.exs
.
You can read more about Plan Configuration below.
config :bankroll,
bling: MyApp.Bling,
bankroll: MyApp.Bankroll,
company_name: "Acme Co",
plans: [
%{
title: "Plus",
description: "Our most popular plan. Good for passing text back and forth.",
features: [
"Unlimited devices"
],
trial_days: 7,
prices: %{
monthly: %{id: "price_1234", price: "$5"},
yearly: %{id: "price_1234", price: "$50"}
}
},
%{
title: "Pro",
description: "For users that would like to share files between devices.",
features: [
"Unlimited devices",
"Media Uploads"
],
prices: %{
monthly: %{id: "price_1234", price: "$10"},
yearly: %{id: "price_1234", price: "$100"}
}
}
]
Open up your lib/my_app/bling.ex
file and configure who can access the billing portal:
defmodule MyApp.Bling do
# ...
def can_manage_billing?(conn, customer) do
conn.assigns.current_user.id == customer.id
end
end
You can now link users to the billing portal from your app:
~p"/billing/user/#{@current_user.id}"
The mix bankroll.install
command should only be ran once.
When you are deploying, you should either commit the assets in priv/static/assets/bankroll
or run the mix bankroll.assets
command during deployment to ensure the required js/css is present.
You can either commit the local ./bankroll
folder you extracted, or git ignore it and ensure you extract it to that location during each deployment.
When new releases are uploaded to the website, you can download the latest release and replace the files in your local bankroll
folder with the new release. Read the release notes to see any specific commands that need to be ran.
Plans are defined in the :bankroll
configuration under the :plans
key.
They can have the following properties:
title
- the plan title shown to the userdescription
- a brief sentence or two to describe who the plan is for or its selling pointsfeatures
- an array of strings representing what is included in the plantrial_days
- (optional) how many trial days to give the user for this planprices
monthly
id
- the stripe price id for the monthly cycleprice
- a string representation of the price, like$10
yearly
id
- the stripe price id for the yearly cycleprice
- a string representation of the price, like$100
config :bankroll, plans: [
%{
title: "Plus",
description: "Our most popular plan. Good for passing text back and forth.",
features: [
"Unlimited devices"
],
trial_days: 7,
prices: %{
monthly: %{id: "price_1234", price: "$5"},
yearly: %{id: "price_1234", price: "$50"}
}
},
%{
title: "Pro",
description: "For users that would like to share files between devices.",
features: [
"Unlimited devices",
"Media Uploads"
],
prices: %{
monthly: %{id: "price_1234", price: "$10"},
yearly: %{id: "price_1234", price: "$100"}
}
}
]
If you'd like to restrict a user from changing to a plan, you can implement the can_subscribe_to_plan?/2
function. It should either return :ok
or {:error, "reason"}
. This can be useful if you want to prevent the user from downgrading to a lower plan if they need to prune something first.
defmodule MyApp.Bankroll do
# ...
def can_subscribe_to_plan?(customer, plan) do
sites = MyApp.Accounts.sites(customer)
if plan[:title] == "Plus" && Enum.count(sites) >= 10 do
{:error, "You must delete some sites first"}
else
:ok
end
end
end
Note: Linking to the portal will create the customer in stripe if it does not exist already.
You can link to the portal using either a route helper or verified routes:
path = MyAppWeb.Router.Helpers.bankroll_root_path(MyAppWeb.Endpoint, :index, "user", current_user.id)
path = ~p"/billing/user/#{current_user.id}"
You can choose who has access to the billing portal by implementing can_manage_billing?
in your Bling module.
Note that the customer
is not the logged in user but is resolved based on the url like /billing/user/4
or /billing/team/2
. You should use the conn
to derive the logged in user to see if they can manage billing for the provided customer
.
defmodule MyApp.Bling do
# ...
def can_manage_billing?(conn, customer) do
user = conn.assigns.current_user
case customer do
%MyApp.Accounts.User{} ->
customer.id == user.id
# %MyApp.Accounts.Team{} -> customer.id == user.team_id
_ ->
false
end
end
end
You should set up payment failure emails as per the Bling docs to notify your users when their subscription payments fail:
https://hexdocs.pm/bling/readme.html#payment-failure-notifications