Packages

phoenix_kit

1.7.52
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules billing web user_billing_profile_form.ex
Raw

lib/modules/billing/web/user_billing_profile_form.ex

defmodule PhoenixKit.Modules.Billing.Web.UserBillingProfileForm do
@moduledoc """
User billing profile form LiveView for creating and editing own billing profiles.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Billing
alias PhoenixKit.Modules.Billing.BillingProfile
alias PhoenixKit.Modules.Billing.CountryData
alias PhoenixKit.Utils.Routes
@impl true
def mount(params, _session, socket) do
user = get_current_user(socket)
cond do
not Billing.enabled?() ->
{:ok,
socket
|> put_flash(:error, "Billing module is not enabled")
|> push_navigate(to: Routes.path("/dashboard"))}
is_nil(user) ->
{:ok,
socket
|> put_flash(:error, "Please log in to manage billing profiles")
|> push_navigate(to: Routes.path("/phoenix_kit/users/log-in"))}
true ->
countries = CountryData.countries_for_select()
return_to = params["return_to"]
socket =
socket
|> assign(:user, user)
|> assign(:countries, countries)
|> assign(:profile_type, "individual")
|> assign(:subdivision_label, "Region")
|> assign(:return_to, return_to)
|> load_profile(params["id"])
{:ok, socket}
end
end
defp load_profile(socket, nil) do
# New profile
changeset = Billing.change_billing_profile(%BillingProfile{type: "individual"})
socket
|> assign(:page_title, "New Billing Profile")
|> assign(:profile, nil)
|> assign(:form, to_form(changeset))
end
defp load_profile(socket, id) do
case Billing.get_billing_profile(id) do
nil ->
socket
|> put_flash(:error, "Billing profile not found")
|> push_navigate(to: Routes.path("/dashboard/billing-profiles"))
profile ->
# Verify ownership
if profile.user_uuid != socket.assigns.user.uuid do
socket
|> put_flash(:error, "Access denied")
|> push_navigate(to: Routes.path("/dashboard/billing-profiles"))
else
changeset = Billing.change_billing_profile(profile)
socket
|> assign(:page_title, "Edit Billing Profile")
|> assign(:profile, profile)
|> assign(:form, to_form(changeset))
|> assign(:profile_type, profile.type)
|> assign(:subdivision_label, CountryData.get_subdivision_label(profile.country))
end
end
end
@impl true
def handle_event("change_type", %{"type" => type}, socket) do
{:noreply, assign(socket, :profile_type, type)}
end
@impl true
def handle_event("validate", %{"billing_profile" => params}, socket) do
changeset =
(socket.assigns.profile || %BillingProfile{})
|> Billing.change_billing_profile(params)
|> Map.put(:action, :validate)
# Update subdivision label when country changes
subdivision_label = CountryData.get_subdivision_label(params["country"])
{:noreply,
socket
|> assign(:form, to_form(changeset))
|> assign(:subdivision_label, subdivision_label)}
end
@impl true
def handle_event("save", %{"billing_profile" => params}, socket) do
params =
params
|> Map.put("user_uuid", socket.assigns.user.uuid)
|> Map.put("type", socket.assigns.profile_type)
save_profile(socket, params)
end
defp save_profile(socket, params) do
result =
if socket.assigns.profile do
Billing.update_billing_profile(socket.assigns.profile, params)
else
Billing.create_billing_profile(socket.assigns.user.uuid, params)
end
case result do
{:ok, _profile} ->
action = if socket.assigns.profile, do: "updated", else: "created"
redirect_path = socket.assigns.return_to || Routes.path("/dashboard/billing-profiles")
{:noreply,
socket
|> put_flash(:info, "Billing profile #{action} successfully")
|> push_navigate(to: redirect_path)}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
@impl true
def render(assigns) do
~H"""
<PhoenixKitWeb.Layouts.dashboard {dashboard_assigns(assigns)}>
<div class="p-6 max-w-3xl mx-auto">
<%!-- Header --%>
<div class="flex items-center gap-4 mb-8">
<.link
navigate={@return_to || Routes.path("/dashboard/billing-profiles")}
class="btn btn-ghost btn-sm"
>
<.icon name="hero-arrow-left" class="w-5 h-5" />
</.link>
<div>
<h1 class="text-2xl font-bold">{@page_title}</h1>
<p class="text-base-content/60 text-sm">
<%= if @profile do %>
Update your billing information
<% else %>
Create a new billing profile for orders
<% end %>
</p>
</div>
</div>
<form phx-change="validate" phx-submit="save" class="space-y-6">
<%!-- Profile Type Selection --%>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-user-circle" class="w-5 h-5" /> Profile Type
</h2>
<div class="flex gap-4 mt-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="profile_type"
value="individual"
class="radio radio-primary"
checked={@profile_type == "individual"}
phx-click="change_type"
phx-value-type="individual"
/>
<span class="label-text">Individual</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="profile_type"
value="company"
class="radio radio-primary"
checked={@profile_type == "company"}
phx-click="change_type"
phx-value-type="company"
/>
<span class="label-text">Company</span>
</label>
</div>
</div>
</div>
<%!-- Individual Fields --%>
<%= if @profile_type == "individual" do %>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-user" class="w-5 h-5" /> Personal Information
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">First Name *</span>
</label>
<input
type="text"
name="billing_profile[first_name]"
value={@form[:first_name].value}
class="input input-bordered"
placeholder="John"
/>
<.error :for={msg <- @form[:first_name].errors |> Enum.map(&elem(&1, 0))}>
{msg}
</.error>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Last Name *</span>
</label>
<input
type="text"
name="billing_profile[last_name]"
value={@form[:last_name].value}
class="input input-bordered"
placeholder="Doe"
/>
<.error :for={msg <- @form[:last_name].errors |> Enum.map(&elem(&1, 0))}>
{msg}
</.error>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
name="billing_profile[email]"
value={@form[:email].value}
class="input input-bordered"
placeholder="john@example.com"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Phone</span>
</label>
<input
type="tel"
name="billing_profile[phone]"
value={@form[:phone].value}
class="input input-bordered"
placeholder="+372 5555 5555"
/>
</div>
</div>
</div>
</div>
<% end %>
<%!-- Company Fields --%>
<%= if @profile_type == "company" do %>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-building-office" class="w-5 h-5" /> Company Information
</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Company Name *</span>
</label>
<input
type="text"
name="billing_profile[company_name]"
value={@form[:company_name].value}
class="input input-bordered"
placeholder="Acme Corp OÜ"
/>
<.error :for={msg <- @form[:company_name].errors |> Enum.map(&elem(&1, 0))}>
{msg}
</.error>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="form-control">
<label class="label">
<span class="label-text">VAT Number</span>
</label>
<input
type="text"
name="billing_profile[company_vat_number]"
value={@form[:company_vat_number].value}
class="input input-bordered font-mono"
placeholder="EE123456789"
/>
<label class="label">
<span class="label-text-alt">EU VAT format: Country code + number</span>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Registration Number</span>
</label>
<input
type="text"
name="billing_profile[company_registration_number]"
value={@form[:company_registration_number].value}
class="input input-bordered font-mono"
placeholder="12345678"
/>
</div>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Legal Address</span>
</label>
<textarea
name="billing_profile[company_legal_address]"
class="textarea textarea-bordered"
rows="2"
placeholder="Registered legal address"
>{@form[:company_legal_address].value}</textarea>
</div>
<div class="divider">Contact</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Contact Email</span>
</label>
<input
type="email"
name="billing_profile[email]"
value={@form[:email].value}
class="input input-bordered"
placeholder="billing@company.com"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Phone</span>
</label>
<input
type="tel"
name="billing_profile[phone]"
value={@form[:phone].value}
class="input input-bordered"
placeholder="+372 5555 5555"
/>
</div>
</div>
</div>
</div>
<% end %>
<%!-- Billing Address (Country FIRST) --%>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-map-pin" class="w-5 h-5" /> Billing Address
</h2>
<%!-- Country first --%>
<div class="form-control">
<label class="label">
<span class="label-text">Country *</span>
</label>
<select name="billing_profile[country]" class="select select-bordered">
<option value="">Select country...</option>
<%= for {name, code} <- @countries do %>
<option value={code} selected={@form[:country].value == code}>
{name}
</option>
<% end %>
</select>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Address Line 1</span>
</label>
<input
type="text"
name="billing_profile[address_line1]"
value={@form[:address_line1].value}
class="input input-bordered"
placeholder="Street address"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Address Line 2</span>
</label>
<input
type="text"
name="billing_profile[address_line2]"
value={@form[:address_line2].value}
class="input input-bordered"
placeholder="Apartment, suite, etc."
/>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div class="form-control">
<label class="label">
<span class="label-text">City</span>
</label>
<input
type="text"
name="billing_profile[city]"
value={@form[:city].value}
class="input input-bordered"
placeholder="Tallinn"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{@subdivision_label}</span>
</label>
<input
type="text"
name="billing_profile[state]"
value={@form[:state].value}
class="input input-bordered"
placeholder="Harju"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Postal Code</span>
</label>
<input
type="text"
name="billing_profile[postal_code]"
value={@form[:postal_code].value}
class="input input-bordered"
placeholder="10115"
/>
</div>
</div>
</div>
</div>
<%!-- Options --%>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg">
<.icon name="hero-cog-6-tooth" class="w-5 h-5" /> Options
</h2>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<input
type="checkbox"
name="billing_profile[is_default]"
value="true"
class="checkbox checkbox-primary"
checked={@form[:is_default].value == true || @form[:is_default].value == "true"}
/>
<div>
<span class="label-text font-medium">Set as default profile</span>
<span class="label-text-alt block">
This profile will be used by default for new orders
</span>
</div>
</label>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Profile Name (Optional)</span>
</label>
<input
type="text"
name="billing_profile[name]"
value={@form[:name].value}
class="input input-bordered"
placeholder="e.g., Home Address, Work"
/>
<label class="label">
<span class="label-text-alt">Custom name to identify this profile</span>
</label>
</div>
</div>
</div>
<%!-- Actions --%>
<div class="flex justify-end gap-4">
<.link
navigate={@return_to || Routes.path("/dashboard/billing-profiles")}
class="btn btn-ghost"
>
Cancel
</.link>
<button type="submit" class="btn btn-primary">
<.icon name="hero-check" class="w-5 h-5 mr-2" />
{if @profile, do: "Save Changes", else: "Create Profile"}
</button>
</div>
</form>
</div>
</PhoenixKitWeb.Layouts.dashboard>
"""
end
# Private helpers
defp get_current_user(socket) do
case socket.assigns[:phoenix_kit_current_scope] do
%{user: %{uuid: _} = user} -> user
_ -> nil
end
end
end