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 usersDownload 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 ./bankrollAdd the dependencies to your mix file:
defp deps do
[
{:stripity_stripe, "~> 2.17"},
{:bling, "~> 0.4.0"},
{:bankroll, path: "./bankroll"}
]
endInstall the dependencies:
mix deps.getNote: 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.installOpen 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
endNow 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
endYou 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 planpricesmonthlyid- the stripe price id for the monthly cycleprice- a string representation of the price, like$10
yearlyid- 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
endNote: 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
endYou 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
