Packages
phoenix_kit
1.7.92
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
Current section
Files
lib/phoenix_kit_web/live/settings/integration_form.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={"#{@project_title} - #{@page_title}"}
current_path={@current_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container mx-auto px-4 py-6 max-w-2xl">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin/settings/integrations")}
title={@page_title}
subtitle={if @provider, do: @provider.name, else: gettext("Choose a service to connect")}
/>
<%!-- Flash messages --%>
<div :if={@success} class="alert alert-success mb-4" phx-click="dismiss">
<span>{@success}</span>
</div>
<div :if={@error} class="alert alert-error mb-4" phx-click="dismiss">
<span>{@error}</span>
</div>
<%!-- Step 1: Provider picker (new mode, no provider selected yet) --%>
<div :if={@live_action == :new && @selected_provider == nil}>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
:for={provider <- @providers}
phx-click="select_provider"
phx-value-provider={provider.key}
class="card bg-base-100 shadow-sm border border-base-300 hover:border-primary transition-colors cursor-pointer"
>
<div class="card-body p-4 flex-row items-center gap-3">
<span class="w-10 h-10 flex items-center justify-center bg-base-200 rounded-lg">
<.icon name={provider.icon} class="w-5 h-5" />
</span>
<div class="text-left">
<div class="font-semibold">{provider.name}</div>
<div class="text-xs text-base-content/60">{provider.description}</div>
</div>
</div>
</button>
</div>
</div>
<%!-- Step 2: Setup form (provider selected in new mode, or edit mode) --%>
<div :if={@provider != nil} class="space-y-6">
<%!-- Back to provider picker (new mode only) --%>
<button
:if={@live_action == :new && @name == nil}
phx-click="back_to_providers"
class="btn btn-ghost btn-sm -mt-2"
>
<.icon name="hero-arrow-left" class="w-4 h-4" />
{gettext("Choose a different service")}
</button>
<%!-- Provider info header with status --%>
<div class="flex items-center gap-3">
<span class="w-10 h-10 flex items-center justify-center bg-base-200 rounded-lg shrink-0">
<.icon name={@provider.icon} class="w-5 h-5" />
</span>
<div class="flex-1">
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">{@provider.name}</h2>
<%= if @data["status"] == "connected" do %>
<span class="badge badge-success badge-sm gap-1">
<.icon name="hero-check-circle" class="w-3 h-3" />
{gettext("Connected")}
</span>
<% end %>
<%= if @data["status"] == "configured" do %>
<span class="badge badge-warning badge-sm">{gettext("Not tested")}</span>
<% end %>
<%= if @data["status"] == "error" do %>
<span class="badge badge-error badge-sm">{gettext("Error")}</span>
<% end %>
<%= if @data["status"] in ["disconnected", nil] do %>
<span class="badge badge-ghost badge-sm">{gettext("Not connected")}</span>
<% end %>
</div>
<p class="text-sm text-base-content/60">
{@provider.description}
<span :if={@data["external_account_id"]} class="ml-1">
— {@data["external_account_id"]}
</span>
</p>
</div>
</div>
<% credentials_saved = @name != nil && has_setup_credentials?(@data, @provider) %>
<%!-- Step 1: Credentials --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-center gap-2">
<span class={"badge badge-sm #{if credentials_saved, do: "badge-success", else: "badge-ghost"}"}>
{gettext("Step 1")}
</span>
<h3 class="card-title text-base">
{if @provider.auth_type == :oauth2,
do: gettext("API Credentials"),
else: gettext("Credentials")}
</h3>
</div>
<p :if={@provider.auth_type == :oauth2} class="text-sm text-base-content/60">
{gettext("Enter your API credentials from the developer console.")}
</p>
<form
phx-submit={if @name, do: "save_setup", else: "create_connection"}
class="space-y-4 mt-2"
>
<%!-- Connection name (new mode only) --%>
<div :if={@name == nil} class="form-control">
<label class="label">
<span class="label-text">{gettext("Connection Name")}</span>
</label>
<input
type="text"
name="name"
value={@new_name}
class="input input-bordered w-full"
placeholder={gettext("default")}
/>
</div>
<%!-- Provider-specific fields --%>
<div :for={field <- @provider.setup_fields} class="form-control">
<label class="label">
<span class="label-text">
{field.label}
<span :if={field.required} class="text-error">*</span>
</span>
</label>
<input
type={if field.type == :password, do: "password", else: "text"}
name={field.key}
value={@data[field.key] || ""}
class="input input-bordered w-full"
placeholder={field.placeholder || ""}
required={field.required}
/>
<label :if={field.help} class="label">
<span class="label-text-alt text-base-content/50">{field.help}</span>
</label>
</div>
<button type="submit" class="btn btn-primary btn-sm">
{gettext("Save Credentials")}
</button>
</form>
</div>
</div>
<%!-- Step 2: Connect Account (OAuth providers only) --%>
<div :if={@provider.auth_type == :oauth2 && @name != nil} class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-center gap-2">
<span class={"badge badge-sm #{if @data["status"] == "connected", do: "badge-success", else: "badge-ghost"}"}>
{gettext("Step 2")}
</span>
<h3 class="card-title text-base">{gettext("Connect Account")}</h3>
</div>
<% meta = @data["metadata"] || %{} %>
<%= if @data["status"] == "connected" do %>
<div class="flex items-center gap-3 mt-3">
<%!-- Avatar --%>
<%= if meta["picture"] do %>
<img
src={meta["picture"]}
class="w-10 h-10 rounded-full"
referrerpolicy="no-referrer"
/>
<% else %>
<div class="w-10 h-10 rounded-full bg-base-200 flex items-center justify-center">
<.icon name="hero-user" class="w-5 h-5 text-base-content/40" />
</div>
<% end %>
<div>
<div class="flex items-center gap-2">
<span :if={meta["name"]} class="font-medium">{meta["name"]}</span>
<div class="badge badge-success badge-sm gap-1">
<.icon name="hero-check-circle" class="w-3 h-3" />
{gettext("Connected")}
</div>
</div>
<span :if={@data["external_account_id"]} class="text-sm text-base-content/60">
{@data["external_account_id"]}
</span>
<span :if={@data["connected_at"]} class="text-xs text-base-content/40 block">
{gettext("Connected on")} {format_date(@data["connected_at"])}
</span>
</div>
</div>
<div class="flex gap-2 mt-3">
<button
type="button"
phx-click="disconnect_account"
class="btn btn-outline btn-error btn-sm"
data-confirm={gettext("Are you sure you want to disconnect this account?")}
>
<.icon name="hero-link-slash" class="w-4 h-4" />
{gettext("Disconnect Account")}
</button>
</div>
<% else %>
<p class="text-sm text-base-content/60 mt-1">
{gettext(
"Authorize access to your %{provider} account. This will open a sign-in page where you choose which account to connect.",
provider: @provider.name
)}
</p>
<%= if credentials_saved do %>
<button
type="button"
phx-click="connect_oauth"
class="btn btn-accent btn-sm mt-3"
>
<.icon name="hero-link" class="w-4 h-4" />
{gettext("Connect %{provider} Account", provider: @provider.name)}
</button>
<% else %>
<p class="text-sm text-base-content/40 mt-2">
{gettext("Complete Step 1 first to enable account connection.")}
</p>
<% end %>
<% end %>
</div>
</div>
<%!-- Test Connection (non-OAuth providers) --%>
<div
:if={
@provider.auth_type != :oauth2 && @name != nil &&
@data["status"] in ["configured", "connected", "error"]
}
class="card bg-base-100 shadow-sm"
>
<div class="card-body">
<div class="flex items-center gap-2">
<span class={"badge badge-sm #{if @data["status"] == "connected", do: "badge-success", else: "badge-ghost"}"}>
{gettext("Step 2")}
</span>
<h3 class="card-title text-base">{gettext("Verify Connection")}</h3>
</div>
<p class="text-sm text-base-content/60 mt-1">
{gettext("Test that your credentials are valid and the service is reachable.")}
</p>
<button
type="button"
phx-click="test_connection"
class={"btn btn-primary btn-sm mt-3 #{if @testing, do: "loading"}"}
disabled={@testing}
>
<.icon :if={!@testing} name="hero-signal" class="w-4 h-4" />
{if @testing, do: gettext("Testing..."), else: gettext("Test Connection")}
</button>
</div>
</div>
<%!-- Setup instructions (from provider definition) --%>
<div :if={Map.get(@provider, :instructions, []) != []} class="card bg-base-200/50">
<div class="card-body text-sm text-base-content/70 space-y-4">
<h3 class="font-semibold text-base-content text-base">
{gettext("Setup Instructions")}
</h3>
<div :for={{section, idx} <- Enum.with_index(Map.get(@provider, :instructions, []))}>
<h4 class="font-semibold text-base-content">
{idx + 1}. {section.title}
</h4>
<p
:if={Map.get(section, :note)}
class="text-xs text-base-content/50 mt-1 ml-2 mb-2"
>
<span class="[&>strong]:font-semibold [&>strong]:text-base-content/70">
{Phoenix.HTML.raw(
render_markdown_inline(section.note, %{"redirect_uri" => @redirect_uri || ""})
)}
</span>
</p>
<ol class="list-decimal list-inside space-y-1 mt-1 ml-2">
<li :for={{text, _detail} <- section.steps}>
<span class="[&>a]:link [&>a]:link-primary [&>strong]:font-semibold [&>strong]:text-base-content">
{Phoenix.HTML.raw(
render_markdown_inline(text, %{"redirect_uri" => @redirect_uri || ""})
)}
</span>
</li>
</ol>
</div>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>