diff --git a/assets/css/app.css b/assets/css/app.css
index c19b7d2..e9234a2 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -102,4 +102,9 @@
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents }
+/* Restore pointer cursor for buttons and links */
+button, a, [role="button"] {
+ cursor: pointer;
+}
+
/* This file is for your main application CSS */
diff --git a/config/dev.exs b/config/dev.exs
index 78aa3a6..df9e6f2 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -87,3 +87,10 @@ config :phoenix_live_view,
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
+
+# Printify API configuration
+# Get your Personal Access Token from: https://printify.com/app/account/api
+config :simpleshop, :printify,
+ access_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzN2Q0YmQzMDM1ZmUxMWU5YTgwM2FiN2VlYjNjY2M5NyIsImp0aSI6IjI1NGYwMjMxMWI2Y2VkNmRhZGQ0ZTg1MmY3MjBkNGNlYjg2ZjZmNWE5NWJlMzUwYWZjNzc4MWYyYjg2ZDA5YzcyMTU1MmJjMDM5YTVlNmYwIiwiaWF0IjoxNzY0NDk4MzcyLjI5MDYzNywibmJmIjoxNzY0NDk4MzcyLjI5MDYzOSwiZXhwIjoxNzk2MDM0MzcyLjI4MTkxNCwic3ViIjoiMjU1NDc5MzIiLCJzY29wZXMiOlsic2hvcHMubWFuYWdlIiwic2hvcHMucmVhZCIsImNhdGFsb2cucmVhZCIsIm9yZGVycy5yZWFkIiwib3JkZXJzLndyaXRlIiwicHJvZHVjdHMucmVhZCIsInByb2R1Y3RzLndyaXRlIiwid2ViaG9va3MucmVhZCIsIndlYmhvb2tzLndyaXRlIiwidXBsb2Fkcy5yZWFkIiwidXBsb2Fkcy53cml0ZSIsInByaW50X3Byb3ZpZGVycy5yZWFkIiwidXNlci5pbmZvIl19.lR9KxY54llZSUj6kNpCjAjdCSfkp8ddRyi0g5sKtVfoaPePFGFl6KA3LXg-uFOvCqhIk9sSEhkUE-UES5jhyKh-uqrhsHLbDZ4m_oXC97aOtbc3nA1IcO_mnlfx863qO7myQ8E33l_ulZP-N0K9JKQKxeGxFgLCr2oFpnmkeca0v4PUL9hewBgXMirUdQgjiBD7CP3kxDiYu4_9Sr6UmsNzVzPKp6X0tuhKf1nvAbAgLLf3jgaoTRCqofNm46bqqXZ9XDmCRrH0HTTY7NtLRwURGfovXQpMdHsfWmz2bn6hITqLILqtmNYsAaXRer7gSq60RyDgu726taA5eDEnUYhpYJC_b6eeAYGq8hTOZqH3AHXeKi_UwbEzsP9a7a3DLOyKiNBmpkYTqeZJdDfX6USKg6ypNDRxXu5HkefkVR2iSfkbg0UCBr-qt-_kM3lPkHbe2kaHlsuNxT1WJfHQ6f9O8GRwJKvcrc7icLqMe9EwsBFzRN9PmKpLch2ofexKR0DaBqb6hrOzZvm8BqfSSMCQ3p5dLX8J7ygGDHpDxvdtc2MPxu57bjPOmcCLqQNd42KjQHmyA3KM9LK1WL3CfW1HHy7-0zfwyclAvEqX0oehqx5w40FbF1c38R9MjEMClOJQefljGXmyBP_e5TjtRcPwBFN182KRwevSRD0PRb68",
+ shop_id: "25499856",
+ base_url: "https://api.printify.com/v1"
diff --git a/lib/simpleshop/printify/client.ex b/lib/simpleshop/printify/client.ex
new file mode 100644
index 0000000..2cc4131
--- /dev/null
+++ b/lib/simpleshop/printify/client.ex
@@ -0,0 +1,124 @@
+defmodule Simpleshop.Printify.Client do
+ require Logger
+ alias Simpleshop.Printify.TokenStore
+
+ @base_url "https://api.printify.com/v1"
+ @user_agent "SimpleshopMVP"
+
+ def get_products do
+ config = Application.get_env(:simpleshop, :printify)
+ shop_id = Keyword.fetch!(config, :shop_id)
+ url = "#{@base_url}/shops/#{shop_id}/products.json"
+
+ with_token(fn token ->
+ case Req.get(url, headers: headers(token)) do
+ {:ok, %{status: 200, body: response}} ->
+ products = Map.get(response, "data") || response
+ Logger.info("Successfully fetched #{length(products)} products")
+ {:ok, products}
+
+ {:ok, %{status: status, body: body}} ->
+ Logger.error("Failed to fetch products: #{status} - #{inspect(body)}")
+ {:error, "Failed to fetch products: #{status}"}
+
+ {:error, error} ->
+ Logger.error("HTTP error fetching products: #{inspect(error)}")
+ {:error, "Network error: #{inspect(error)}"}
+ end
+ end)
+ end
+
+ def get_product(product_id) do
+ config = Application.get_env(:simpleshop, :printify)
+ shop_id = Keyword.fetch!(config, :shop_id)
+ url = "#{@base_url}/shops/#{shop_id}/products/#{product_id}.json"
+
+ with_token(fn token ->
+ case Req.get(url, headers: headers(token)) do
+ {:ok, %{status: 200, body: product}} ->
+ Logger.info("Successfully fetched product #{product_id}")
+ {:ok, product}
+
+ {:ok, %{status: status, body: body}} ->
+ Logger.error("Failed to fetch product: #{status} - #{inspect(body)}")
+ {:error, "Failed to fetch product: #{status}"}
+
+ {:error, error} ->
+ Logger.error("HTTP error fetching product: #{inspect(error)}")
+ {:error, "Network error: #{inspect(error)}"}
+ end
+ end)
+ end
+
+ def create_order(product_id, variant_id, quantity \\ 1) do
+ config = Application.get_env(:simpleshop, :printify)
+ shop_id = Keyword.fetch!(config, :shop_id)
+ url = "#{@base_url}/shops/#{shop_id}/orders.json"
+
+ order_data = %{
+ external_id: "simpleshop-#{System.unique_integer([:positive])}",
+ line_items: [
+ %{
+ product_id: product_id,
+ variant_id: variant_id,
+ quantity: quantity
+ }
+ ],
+ shipping_method: 1,
+ address_to: test_shipping_address()
+ }
+
+ with_token(fn token ->
+ case Req.post(url, headers: headers(token), json: order_data) do
+ {:ok, %{status: status, body: response}} when status in 200..299 ->
+ Logger.info("Successfully created order: #{inspect(response)}")
+ {:ok, response}
+
+ {:ok, %{status: status, body: body}} ->
+ Logger.error("Failed to create order: #{status} - #{inspect(body)}")
+ {:error, "Failed to create order: #{status} - #{inspect(body)}"}
+
+ {:error, error} ->
+ Logger.error("HTTP error creating order: #{inspect(error)}")
+ {:error, "Network error: #{inspect(error)}"}
+ end
+ end)
+ end
+
+ defp with_token(fun) do
+ case TokenStore.get_access_token() do
+ {:ok, token} ->
+ fun.(token)
+
+ {:error, :not_configured} ->
+ Logger.error("Printify Personal Access Token not configured in config/dev.exs")
+ {:error, :not_configured}
+
+ {:error, reason} ->
+ Logger.error("Failed to get access token: #{inspect(reason)}")
+ {:error, :authentication_required}
+ end
+ end
+
+ defp headers(token) do
+ [
+ {"Authorization", "Bearer #{token}"},
+ {"User-Agent", @user_agent},
+ {"Content-Type", "application/json;charset=utf-8"}
+ ]
+ end
+
+ defp test_shipping_address do
+ %{
+ first_name: "John",
+ last_name: "Doe",
+ email: "test@example.com",
+ phone: "555-0123",
+ country: "US",
+ region: "CA",
+ address1: "123 Test St",
+ city: "San Francisco",
+ zip: "94102"
+ }
+ end
+end
diff --git a/lib/simpleshop/printify/oauth.ex b/lib/simpleshop/printify/oauth.ex
new file mode 100644
index 0000000..5d1199d
--- /dev/null
+++ b/lib/simpleshop/printify/oauth.ex
@@ -0,0 +1,92 @@
+defmodule Simpleshop.Printify.OAuth do
+ require Logger
+ alias Simpleshop.Printify.TokenStore
+
+ @printify_auth_url "https://printify.com/app/authorize"
+ @printify_token_url "https://api.printify.com/v1/app/oauth/tokens"
+ @printify_refresh_url "https://api.printify.com/v1/app/oauth/tokens/refresh"
+
+ def authorization_url do
+ config = Application.get_env(:simpleshop, :printify)
+ app_id = Keyword.fetch!(config, :app_id)
+ redirect_uri = Keyword.fetch!(config, :redirect_uri)
+
+ query =
+ URI.encode_query(%{
+ app_id: app_id,
+ accept_url: redirect_uri,
+ decline_url: redirect_uri <> "?error=access_denied"
+ })
+
+ "#{@printify_auth_url}?#{query}"
+ end
+
+ def exchange_code(code) do
+ config = Application.get_env(:simpleshop, :printify)
+ app_id = Keyword.fetch!(config, :app_id)
+
+ body = %{
+ app_id: app_id,
+ code: code
+ }
+
+ case Req.post(@printify_token_url, json: body) do
+ {:ok, %{status: 200, body: response}} ->
+ Logger.info("Successfully exchanged OAuth code for tokens")
+ store_token_response(response)
+
+ {:ok, %{status: status, body: body}} ->
+ Logger.error("Failed to exchange OAuth code: #{status} - #{inspect(body)}")
+ {:error, "Failed to get access token: #{inspect(body)}"}
+
+ {:error, error} ->
+ Logger.error("HTTP error during token exchange: #{inspect(error)}")
+ {:error, "Network error: #{inspect(error)}"}
+ end
+ end
+
+ def refresh_access_token do
+ case TokenStore.get_refresh_token() do
+ {:ok, refresh_token} ->
+ config = Application.get_env(:simpleshop, :printify)
+ app_id = Keyword.fetch!(config, :app_id)
+
+ body = %{
+ app_id: app_id,
+ refresh_token: refresh_token
+ }
+
+ case Req.post(@printify_refresh_url, json: body) do
+ {:ok, %{status: 200, body: response}} ->
+ Logger.info("Successfully refreshed access token")
+ store_token_response(response)
+
+ {:ok, %{status: status, body: body}} ->
+ Logger.error("Failed to refresh access token: #{status} - #{inspect(body)}")
+ {:error, "Failed to refresh token: #{inspect(body)}"}
+
+ {:error, error} ->
+ Logger.error("HTTP error during token refresh: #{inspect(error)}")
+ {:error, "Network error: #{inspect(error)}"}
+ end
+
+ {:error, :not_found} ->
+ Logger.error("No refresh token found")
+ {:error, :no_refresh_token}
+ end
+ end
+
+ defp store_token_response(response) do
+ access_token = Map.get(response, "access_token") || Map.get(response, :access_token)
+ refresh_token = Map.get(response, "refresh_token") || Map.get(response, :refresh_token)
+ expires_in = Map.get(response, "expires_in") || Map.get(response, :expires_in) || 21_600
+
+ if access_token && refresh_token do
+ TokenStore.store_tokens(access_token, refresh_token, expires_in)
+ {:ok, access_token}
+ else
+ Logger.error("Missing tokens in response: #{inspect(response)}")
+ {:error, "Invalid token response"}
+ end
+ end
+end
diff --git a/lib/simpleshop/printify/token_store.ex b/lib/simpleshop/printify/token_store.ex
new file mode 100644
index 0000000..83ac449
--- /dev/null
+++ b/lib/simpleshop/printify/token_store.ex
@@ -0,0 +1,24 @@
+defmodule Simpleshop.Printify.TokenStore do
+ @moduledoc """
+ Simple module to retrieve the Printify Personal Access Token from config.
+ No GenServer or ETS needed - just reads from application config.
+ """
+
+ def get_access_token do
+ config = Application.get_env(:simpleshop, :printify)
+
+ case Keyword.get(config, :access_token) do
+ nil -> {:error, :not_configured}
+ "" -> {:error, :not_configured}
+ "your_personal_access_token_here" -> {:error, :not_configured}
+ token -> {:ok, token}
+ end
+ end
+
+ def has_tokens? do
+ case get_access_token() do
+ {:ok, _} -> true
+ _ -> false
+ end
+ end
+end
diff --git a/lib/simpleshop_web/components/layouts/root.html.heex b/lib/simpleshop_web/components/layouts/root.html.heex
index 626656b..2c05dfd 100644
--- a/lib/simpleshop_web/components/layouts/root.html.heex
+++ b/lib/simpleshop_web/components/layouts/root.html.heex
@@ -15,7 +15,8 @@
const setTheme = (theme) => {
if (theme === "system") {
localStorage.removeItem("phx:theme");
- document.documentElement.removeAttribute("data-theme");
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ document.documentElement.setAttribute("data-theme", prefersDark ? "dark" : "light");
} else {
localStorage.setItem("phx:theme", theme);
document.documentElement.setAttribute("data-theme", theme);
@@ -25,7 +26,7 @@
setTheme(localStorage.getItem("phx:theme") || "system");
}
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
-
+
window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
})();
diff --git a/lib/simpleshop_web/controllers/oauth_controller.ex b/lib/simpleshop_web/controllers/oauth_controller.ex
new file mode 100644
index 0000000..ad6e043
--- /dev/null
+++ b/lib/simpleshop_web/controllers/oauth_controller.ex
@@ -0,0 +1,9 @@
+defmodule SimpleshopWeb.OAuthController do
+ use SimpleshopWeb, :controller
+ alias Simpleshop.Printify.OAuth
+
+ def authorize(conn, _params) do
+ auth_url = OAuth.authorization_url()
+ redirect(conn, external: auth_url)
+ end
+end
diff --git a/lib/simpleshop_web/controllers/page_html/home.html.heex b/lib/simpleshop_web/controllers/page_html/home.html.heex
index b107fd0..7e70b6c 100644
--- a/lib/simpleshop_web/controllers/page_html/home.html.heex
+++ b/lib/simpleshop_web/controllers/page_html/home.html.heex
@@ -1,202 +1,59 @@
-
+ Printify Integration MVP +
++ Browse products and create test orders with your Printify catalog +
-- Peace of mind from prototype to production. -
-- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -
-
+ Setup Required: Add your Personal Access Token to config/dev.exs
+
+ Get your token at: printify.com/app/account/api +
++ This is a proof-of-concept integration using Personal Access Token +
Fetching your shop information...
+<%= @error %>
+ + <%= if @response_details do %> +Debug Information:
+Status Code: <%= @response_details.status %>
+Error Message: <%= @response_details.error_msg %>
+<%= Jason.encode!(@response_details.body, pretty: true) %>+
Troubleshooting Tips:
+No shops found for this account.
+Shop ID (Use this!)
++ <%= shop["id"] %> +
+Sales Channel
+<%= shop["sales_channel"] || "N/A" %>
+
+ Next step: Copy the Shop ID above and paste it into
+ config/dev.exs
+
shop_id: "<%= shop["id"] %>",+
<%= Jason.encode!(shop, pretty: true) %>+
Authentication Failed
++ We couldn't complete the connection to Printify. +
+ <.link + navigate={~p"/"} + class="inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700" + > + Return Home + +<%= @error %>+
Redirecting to products...
+ <% end %> +Your test order has been submitted to Printify
+<%= address["first_name"] %> <%= address["last_name"] %>
+<%= address["address1"] %>
+ <%= if address["address2"] do %> +<%= address["address2"] %>
+ <% end %> ++ <%= address["city"] %>, <%= address["region"] %> <%= address["zip"] %> +
+<%= address["country"] %>
+ <%= if address["email"] do %> +Email: <%= address["email"] %>
+ <% end %> + <%= if address["phone"] do %> +Phone: <%= address["phone"] %>
+ <% end %> +<%= Jason.encode!(@order_data, pretty: true) %>+
+ Next steps: + Check your Printify dashboard to see this order and manage its fulfillment. +
++ This was a test order with a hardcoded shipping address. +
+Loading products...
++ You need to add your Printify Personal Access Token to the configuration file. +
+ +Steps:
+config/dev.exs
+ your_personal_access_token_here with your actual tokenshop_id<%= @error %>
+No products found in your Printify shop.
+Add products in your Printify dashboard first.
++ <%= product["description"] || "No description" %> +
+ +<%= @product["description"] || "No description available" %>
+ + <%= if @variant_options && map_size(@variant_options) > 0 do %> ++ <%= format_price(variant["price"]) %> +
++ Variant ID: <%= variant["id"] %> +
++ This will create a test order with hardcoded shipping address +
+