make admin provider UI support both Printify and Printful
- Provider form accepts ?type= query param (printify/printful) - Conditional setup instructions per provider (API key steps, login URLs) - Dynamic labels, titles, and config handling (shop_id vs store_id) - Provider index shows dropdown with both provider options - Settings page renamed from @printify to @provider (generic) - Fix Printful shipping rates: add default state codes for US/CA/AU Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
24d61f7a9e
commit
61cb2b7a87
@ -198,8 +198,9 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
|
||||
defp fetch_rate_for_product(catalog_product_id, variant_id, country_code) do
|
||||
items = [%{source: "catalog", catalog_variant_id: variant_id, quantity: 1}]
|
||||
recipient = build_recipient(country_code)
|
||||
|
||||
case Client.calculate_shipping(%{country_code: country_code}, items) do
|
||||
case Client.calculate_shipping(recipient, items) do
|
||||
{:ok, rates} when is_list(rates) ->
|
||||
standard = Enum.find(rates, &(&1["shipping"] == "STANDARD")) || List.first(rates)
|
||||
|
||||
@ -228,6 +229,18 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
end
|
||||
end
|
||||
|
||||
# Printful requires state_code for US, CA, and AU
|
||||
@default_state_codes %{"US" => "NY", "CA" => "ON", "AU" => "NSW"}
|
||||
|
||||
defp build_recipient(country_code) do
|
||||
base = %{country_code: country_code}
|
||||
|
||||
case @default_state_codes[country_code] do
|
||||
nil -> base
|
||||
state -> Map.put(base, :state_code, state)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns {catalog_product_id, first_catalog_variant_id} per product
|
||||
defp extract_per_product_items(products) do
|
||||
products
|
||||
|
||||
@ -5,15 +5,20 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
alias SimpleshopTheme.Products.ProviderConnection
|
||||
alias SimpleshopTheme.Providers
|
||||
|
||||
@supported_types ~w(printify printful)
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
{:ok, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
defp apply_action(socket, :new, params) do
|
||||
provider_type = validated_type(params["type"])
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "Connect to Printify")
|
||||
|> assign(:connection, %ProviderConnection{provider_type: "printify"})
|
||||
|> assign(:page_title, "Connect to #{provider_label(provider_type)}")
|
||||
|> assign(:provider_type, provider_type)
|
||||
|> assign(:connection, %ProviderConnection{provider_type: provider_type})
|
||||
|> assign(:form, to_form(ProviderConnection.changeset(%ProviderConnection{}, %{})))
|
||||
|> assign(:testing, false)
|
||||
|> assign(:test_result, nil)
|
||||
@ -24,7 +29,8 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
connection = Products.get_provider_connection!(id)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "Printify settings")
|
||||
|> assign(:page_title, "#{provider_label(connection.provider_type)} settings")
|
||||
|> assign(:provider_type, connection.provider_type)
|
||||
|> assign(:connection, connection)
|
||||
|> assign(:form, to_form(ProviderConnection.changeset(connection, %{})))
|
||||
|> assign(:testing, false)
|
||||
@ -48,14 +54,13 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
def handle_event("test_connection", _params, socket) do
|
||||
socket = assign(socket, testing: true, test_result: nil)
|
||||
|
||||
# Use pending_api_key from validation, or fall back to existing encrypted key
|
||||
api_key =
|
||||
socket.assigns[:pending_api_key] ||
|
||||
ProviderConnection.get_api_key(socket.assigns.connection)
|
||||
|
||||
if api_key && api_key != "" do
|
||||
temp_conn = %ProviderConnection{
|
||||
provider_type: "printify",
|
||||
provider_type: socket.assigns.provider_type,
|
||||
api_key_encrypted: encrypt_api_key(api_key)
|
||||
}
|
||||
|
||||
@ -72,17 +77,19 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
end
|
||||
|
||||
defp save_connection(socket, :new, params) do
|
||||
provider_type = socket.assigns.provider_type
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.put("provider_type", "printify")
|
||||
|> maybe_add_shop_config(socket.assigns.test_result)
|
||||
|> maybe_add_name(socket.assigns.test_result)
|
||||
|> Map.put("provider_type", provider_type)
|
||||
|> maybe_add_config(provider_type, socket.assigns.test_result)
|
||||
|> maybe_add_name(provider_type, socket.assigns.test_result)
|
||||
|
||||
case Products.create_provider_connection(params) do
|
||||
{:ok, _connection} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Connected to Printify!")
|
||||
|> put_flash(:info, "Connected to #{provider_label(provider_type)}!")
|
||||
|> push_navigate(to: ~p"/admin/settings")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
@ -103,19 +110,29 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_shop_config(params, {:ok, %{shop_id: shop_id}}) do
|
||||
# Printify returns shop_id, Printful returns store_id
|
||||
defp maybe_add_config(params, "printify", {:ok, %{shop_id: shop_id}}) do
|
||||
config = Map.get(params, "config", %{}) |> Map.put("shop_id", to_string(shop_id))
|
||||
Map.put(params, "config", config)
|
||||
end
|
||||
|
||||
defp maybe_add_shop_config(params, _), do: params
|
||||
|
||||
defp maybe_add_name(params, {:ok, %{shop_name: shop_name}}) when is_binary(shop_name) do
|
||||
Map.put_new(params, "name", shop_name)
|
||||
defp maybe_add_config(params, "printful", {:ok, %{store_id: store_id}}) do
|
||||
config = Map.get(params, "config", %{}) |> Map.put("store_id", to_string(store_id))
|
||||
Map.put(params, "config", config)
|
||||
end
|
||||
|
||||
defp maybe_add_name(params, _) do
|
||||
Map.put_new(params, "name", "Printify")
|
||||
defp maybe_add_config(params, _type, _result), do: params
|
||||
|
||||
defp maybe_add_name(params, "printify", {:ok, %{shop_name: name}}) when is_binary(name) do
|
||||
Map.put_new(params, "name", name)
|
||||
end
|
||||
|
||||
defp maybe_add_name(params, "printful", {:ok, %{store_name: name}}) when is_binary(name) do
|
||||
Map.put_new(params, "name", name)
|
||||
end
|
||||
|
||||
defp maybe_add_name(params, type, _result) do
|
||||
Map.put_new(params, "name", provider_label(type))
|
||||
end
|
||||
|
||||
defp encrypt_api_key(api_key) do
|
||||
@ -125,9 +142,21 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do
|
||||
end
|
||||
end
|
||||
|
||||
defp format_error(:no_api_key), do: "Please enter your connection key"
|
||||
defp validated_type(type) when type in @supported_types, do: type
|
||||
defp validated_type(_), do: "printify"
|
||||
|
||||
# Shared helpers used by the template
|
||||
|
||||
defp provider_label("printful"), do: "Printful"
|
||||
defp provider_label(_), do: "Printify"
|
||||
|
||||
defp connection_name({:ok, %{shop_name: name}}), do: name
|
||||
defp connection_name({:ok, %{store_name: name}}), do: name
|
||||
defp connection_name(_), do: nil
|
||||
|
||||
defp format_error(:no_api_key), do: "Please enter your API key"
|
||||
defp format_error(:unauthorized), do: "That key doesn't seem to be valid"
|
||||
defp format_error(:timeout), do: "Couldn't reach Printify - try again"
|
||||
defp format_error(:timeout), do: "Couldn't reach the provider - try again"
|
||||
defp format_error({:http_error, _code}), do: "Something went wrong - try again"
|
||||
defp format_error(error) when is_binary(error), do: error
|
||||
defp format_error(_), do: "Connection failed - check your key and try again"
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
<.header>
|
||||
{if @live_action == :new, do: "Connect to Printify", else: "Printify settings"}
|
||||
{if @live_action == :new,
|
||||
do: "Connect to #{provider_label(@provider_type)}",
|
||||
else: "#{provider_label(@provider_type)} settings"}
|
||||
</.header>
|
||||
|
||||
<div class="max-w-xl mt-6">
|
||||
<%= if @live_action == :new do %>
|
||||
<div class="prose prose-sm mb-6">
|
||||
<p>
|
||||
Printify is a print-on-demand service that prints and ships products for you.
|
||||
{provider_label(@provider_type)} is a print-on-demand service that prints and ships products for you.
|
||||
Connect your account to automatically import your products into your shop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= if @provider_type == "printify" do %>
|
||||
<div class="rounded-lg bg-base-200 p-4 mb-6 text-sm">
|
||||
<p class="font-medium mb-2">Get your connection key from Printify:</p>
|
||||
<p class="font-medium mb-2">Get your API key from Printify:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-base-content/80">
|
||||
<li>
|
||||
<a
|
||||
@ -40,15 +43,42 @@
|
||||
<li>Click <strong>Copy to clipboard</strong> and paste it below</li>
|
||||
</ol>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="rounded-lg bg-base-200 p-4 mb-6 text-sm">
|
||||
<p class="font-medium mb-2">Get your API key from Printful:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-base-content/80">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.printful.com/auth/login"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>
|
||||
Log in to Printful
|
||||
</a>
|
||||
(or <a
|
||||
href="https://www.printful.com/auth/signup"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>create a free account</a>)
|
||||
</li>
|
||||
<li>Go to <strong>Settings</strong> → <strong>API access</strong></li>
|
||||
<li>Click <strong>Create API key</strong></li>
|
||||
<li>Give it a name and select <strong>all scopes</strong></li>
|
||||
<li>Copy the token and paste it below</li>
|
||||
</ol>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<.form for={@form} id="provider-form" phx-change="validate" phx-submit="save">
|
||||
<input type="hidden" name="provider_connection[provider_type]" value="printify" />
|
||||
<input type="hidden" name="provider_connection[provider_type]" value={@provider_type} />
|
||||
|
||||
<.input
|
||||
field={@form[:api_key]}
|
||||
type="password"
|
||||
label="Printify connection key"
|
||||
label={"#{provider_label(@provider_type)} API key"}
|
||||
placeholder={
|
||||
if @live_action == :edit,
|
||||
do: "Leave blank to keep current key",
|
||||
@ -73,9 +103,10 @@
|
||||
|
||||
<div :if={@test_result} class="text-sm">
|
||||
<%= case @test_result do %>
|
||||
<% {:ok, info} -> %>
|
||||
<% {:ok, _info} -> %>
|
||||
<span class="text-success flex items-center gap-1">
|
||||
<.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name}
|
||||
<.icon name="hero-check-circle" class="size-4" />
|
||||
Connected to {connection_name(@test_result) || provider_label(@provider_type)}
|
||||
</span>
|
||||
<% {:error, reason} -> %>
|
||||
<span class="text-error flex items-center gap-1">
|
||||
@ -92,7 +123,9 @@
|
||||
|
||||
<div class="flex gap-2 mt-6">
|
||||
<.button type="submit" disabled={@testing}>
|
||||
{if @live_action == :new, do: "Connect to Printify", else: "Save changes"}
|
||||
{if @live_action == :new,
|
||||
do: "Connect to #{provider_label(@provider_type)}",
|
||||
else: "Save changes"}
|
||||
</.button>
|
||||
<.link navigate={~p"/admin/providers"} class="btn btn-ghost">
|
||||
Cancel
|
||||
|
||||
@ -1,23 +1,38 @@
|
||||
<.header>
|
||||
Providers
|
||||
<:actions>
|
||||
<.button navigate={~p"/admin/providers/new"}>
|
||||
<.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify
|
||||
</.button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-primary">
|
||||
<.icon name="hero-plus" class="size-4 mr-1" /> Connect provider
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-52 z-10">
|
||||
<li>
|
||||
<.link navigate={~p"/admin/providers/new?type=printify"}>Printify</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate={~p"/admin/providers/new?type=printful"}>Printful</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div id="connections" phx-update="stream" class="mt-6 space-y-4">
|
||||
<div id="connections-empty" class="hidden only:block text-center py-12">
|
||||
<.icon name="hero-cube" class="size-16 mx-auto mb-4 text-base-content/30" />
|
||||
<h2 class="text-xl font-medium">Connect your Printify account</h2>
|
||||
<h2 class="text-xl font-medium">Connect a print-on-demand provider</h2>
|
||||
<p class="mt-2 text-base-content/60 max-w-md mx-auto">
|
||||
Printify handles printing and shipping for you. Connect your account
|
||||
to import your products and start selling.
|
||||
Connect your Printify or Printful account to import products
|
||||
and start selling.
|
||||
</p>
|
||||
<.button navigate={~p"/admin/providers/new"} class="mt-6">
|
||||
Connect to Printify
|
||||
<div class="flex justify-center gap-3 mt-6">
|
||||
<.button navigate={~p"/admin/providers/new?type=printify"}>
|
||||
Connect Printify
|
||||
</.button>
|
||||
<.button navigate={~p"/admin/providers/new?type=printful"} class="btn-outline">
|
||||
Connect Printful
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -50,7 +65,7 @@
|
||||
<button
|
||||
phx-click="delete"
|
||||
phx-value-id={connection.id}
|
||||
data-confirm="Disconnect from Printify? Your synced products will remain in your shop."
|
||||
data-confirm={"Disconnect from #{String.capitalize(connection.provider_type)}? Your synced products will remain in your shop."}
|
||||
class="btn btn-ghost btn-sm text-error"
|
||||
>
|
||||
Disconnect
|
||||
|
||||
@ -59,7 +59,7 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
nil
|
||||
end
|
||||
|
||||
assign(socket, :printify, connection_info)
|
||||
assign(socket, :provider, connection_info)
|
||||
end
|
||||
|
||||
# -- Account assigns --
|
||||
@ -339,7 +339,7 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Products</h2>
|
||||
<%= if @printify do %>
|
||||
<%= if @provider do %>
|
||||
<.status_pill color="green">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
|
||||
</.status_pill>
|
||||
@ -348,8 +348,8 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if @printify do %>
|
||||
<.printify_connected printify={@printify} />
|
||||
<%= if @provider do %>
|
||||
<.provider_connected provider={@provider} />
|
||||
<% else %>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">
|
||||
@ -357,10 +357,10 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<.link
|
||||
navigate={~p"/admin/providers/new"}
|
||||
navigate={~p"/admin/providers"}
|
||||
class="inline-flex items-center gap-2 rounded-md bg-base-content px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-base-content/80"
|
||||
>
|
||||
<.icon name="hero-plus-mini" class="size-4" /> Connect to Printify
|
||||
<.icon name="hero-plus-mini" class="size-4" /> Connect a provider
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
@ -474,23 +474,24 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
"""
|
||||
end
|
||||
|
||||
attr :printify, :map, required: true
|
||||
attr :provider, :map, required: true
|
||||
|
||||
defp printify_connected(assigns) do
|
||||
conn = assigns.printify.connection
|
||||
defp provider_connected(assigns) do
|
||||
conn = assigns.provider.connection
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:connection, conn)
|
||||
|> assign(:product_count, assigns.printify.product_count)
|
||||
|> assign(:product_count, assigns.provider.product_count)
|
||||
|> assign(:syncing, conn.sync_status == "syncing")
|
||||
|> assign(:provider_label, String.capitalize(conn.provider_type))
|
||||
|
||||
~H"""
|
||||
<div class="mt-4">
|
||||
<dl class="text-sm">
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Provider</dt>
|
||||
<dd class="text-base-content">Printify</dd>
|
||||
<dd class="text-base-content">{@provider_label}</dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Shop</dt>
|
||||
@ -534,7 +535,7 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
<button
|
||||
phx-click="delete_connection"
|
||||
phx-value-id={@connection.id}
|
||||
data-confirm="Disconnect from Printify? Your synced products will remain in your shop."
|
||||
data-confirm={"Disconnect from #{@provider_label}? Your synced products will remain in your shop."}
|
||||
class="text-sm text-red-600 hover:text-red-800 px-2 py-1.5"
|
||||
>
|
||||
Disconnect
|
||||
|
||||
@ -33,13 +33,14 @@ defmodule SimpleshopThemeWeb.Admin.ProvidersTest do
|
||||
test "shows empty state when no connections exist", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/providers")
|
||||
|
||||
assert html =~ "Connect your Printify account"
|
||||
assert html =~ "Connect a print-on-demand provider"
|
||||
end
|
||||
|
||||
test "shows connect button", %{conn: conn} do
|
||||
test "shows connect buttons for both providers", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/providers")
|
||||
|
||||
assert has_element?(view, ~s(a[href="/admin/providers/new"]))
|
||||
assert has_element?(view, ~s(a[href="/admin/providers/new?type=printify"]))
|
||||
assert has_element?(view, ~s(a[href="/admin/providers/new?type=printful"]))
|
||||
end
|
||||
end
|
||||
|
||||
@ -138,12 +139,26 @@ defmodule SimpleshopThemeWeb.Admin.ProvidersTest do
|
||||
%{conn: log_in_user(conn, user)}
|
||||
end
|
||||
|
||||
test "renders new form", %{conn: conn} do
|
||||
test "renders new Printify form", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/providers/new?type=printify")
|
||||
|
||||
assert html =~ "Connect to Printify"
|
||||
assert html =~ "Printify API key"
|
||||
assert html =~ "Log in to Printify"
|
||||
end
|
||||
|
||||
test "renders new Printful form", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/providers/new?type=printful")
|
||||
|
||||
assert html =~ "Connect to Printful"
|
||||
assert html =~ "Printful API key"
|
||||
assert html =~ "Log in to Printful"
|
||||
end
|
||||
|
||||
test "defaults to Printify when no type param", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/providers/new")
|
||||
|
||||
assert html =~ "Connect to Printify"
|
||||
assert html =~ "connection key"
|
||||
assert html =~ "Log in to Printify"
|
||||
end
|
||||
|
||||
test "test connection shows error when no api key", %{conn: conn} do
|
||||
@ -151,7 +166,7 @@ defmodule SimpleshopThemeWeb.Admin.ProvidersTest do
|
||||
|
||||
html = render_click(view, "test_connection")
|
||||
|
||||
assert html =~ "Please enter your connection key"
|
||||
assert html =~ "Please enter your API key"
|
||||
end
|
||||
|
||||
test "saves new connection", %{conn: conn} do
|
||||
|
||||
@ -151,7 +151,7 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do
|
||||
|
||||
assert html =~ "Products"
|
||||
assert html =~ "Not connected"
|
||||
assert has_element?(view, ~s(a[href="/admin/providers/new"]), "Connect to Printify")
|
||||
assert has_element?(view, ~s(a[href="/admin/providers"]), "Connect a provider")
|
||||
end
|
||||
|
||||
test "shows connection info when provider connected", %{conn: conn} do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user