extract site_name and site_description from theme settings into standalone settings
site_name and site_description are shop identity, not theme concerns. They now live in the Settings table as first-class settings with their own assigns (@site_name, @site_description) piped through hooks and plugs. The setup wizard writes site_name on account creation, and the theme editor reads/writes via Settings.put_setting. Removed the "configure your shop" checklist item since currency/country aren't built yet. Also adds shop name field to setup wizard step 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ea77e5992
commit
5b41f3fedf
@ -1384,6 +1384,11 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.setup-test-help {
|
||||
margin: 0.375rem 0 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Card radio group — selectable cards backed by radio inputs */
|
||||
.card-radio-fieldset {
|
||||
border: none;
|
||||
@ -1528,7 +1533,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.admin-checklist-title {
|
||||
@ -1570,17 +1583,48 @@
|
||||
|
||||
.admin-checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.admin-checklist-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-checklist-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-checklist-hint {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--admin-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-checklist-optional {
|
||||
display: inline-block;
|
||||
margin-left: 0.375rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
background: var(--t-surface-sunken, #e5e5e5);
|
||||
color: var(--admin-text-muted);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-checklist-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-top: 0.125rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
border: 1.5px solid var(--t-border-default, #d4d4d4);
|
||||
@ -1594,7 +1638,6 @@
|
||||
}
|
||||
|
||||
.admin-checklist-label {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@ -1606,13 +1649,9 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-checklist-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--t-surface-sunken, #e5e5e5);
|
||||
/* When collapsed, the header has no bottom margin */
|
||||
.admin-checklist-header:only-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ── Page editor ── */
|
||||
|
||||
@ -27,7 +27,7 @@ defmodule Berrypod.Release do
|
||||
|
||||
case Settings.get_setting("theme_settings") do
|
||||
nil ->
|
||||
{:ok, _} = Settings.apply_preset(:studio)
|
||||
{:ok, _} = Settings.apply_preset(:studio, skip_customised_flag: true)
|
||||
:ok
|
||||
|
||||
_exists ->
|
||||
|
||||
@ -74,7 +74,6 @@ defmodule Berrypod.Settings do
|
||||
def get_theme_settings do
|
||||
case get_setting("theme_settings") do
|
||||
nil ->
|
||||
# Return defaults
|
||||
%ThemeSettings{}
|
||||
|
||||
settings_map when is_map(settings_map) ->
|
||||
@ -84,6 +83,16 @@ defmodule Berrypod.Settings do
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Returns the shop name from Settings, falling back to a default."
|
||||
def site_name do
|
||||
get_setting("site_name") || "Store Name"
|
||||
end
|
||||
|
||||
@doc "Returns the shop description from Settings, falling back to a default."
|
||||
def site_description do
|
||||
get_setting("site_description") || "Discover unique products and original designs."
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the theme settings.
|
||||
|
||||
@ -93,7 +102,7 @@ defmodule Berrypod.Settings do
|
||||
{:ok, %ThemeSettings{}}
|
||||
|
||||
"""
|
||||
def update_theme_settings(attrs) when is_map(attrs) do
|
||||
def update_theme_settings(attrs, opts \\ []) when is_map(attrs) do
|
||||
current = get_theme_settings()
|
||||
|
||||
changeset = ThemeSettings.changeset(current, attrs)
|
||||
@ -102,7 +111,10 @@ defmodule Berrypod.Settings do
|
||||
settings = Ecto.Changeset.apply_changes(changeset)
|
||||
json = Jason.encode!(settings)
|
||||
put_setting("theme_settings", json, "json")
|
||||
put_setting("theme_customised", true, "boolean")
|
||||
|
||||
unless opts[:skip_customised_flag] do
|
||||
put_setting("theme_customised", true, "boolean")
|
||||
end
|
||||
|
||||
# Invalidate and rewarm CSS cache
|
||||
alias Berrypod.Theme.{CSSCache, CSSGenerator}
|
||||
@ -125,11 +137,11 @@ defmodule Berrypod.Settings do
|
||||
{:ok, %ThemeSettings{}}
|
||||
|
||||
"""
|
||||
def apply_preset(preset_name) when is_atom(preset_name) do
|
||||
def apply_preset(preset_name, opts \\ []) when is_atom(preset_name) do
|
||||
preset = Berrypod.Theme.Presets.get(preset_name)
|
||||
|
||||
if preset do
|
||||
update_theme_settings(preset)
|
||||
update_theme_settings(preset, opts)
|
||||
else
|
||||
{:error, :preset_not_found}
|
||||
end
|
||||
|
||||
@ -15,8 +15,6 @@ defmodule Berrypod.Settings.ThemeSettings do
|
||||
field :accent_color, :string, default: "#f97316"
|
||||
|
||||
# Branding
|
||||
field :site_name, :string, default: "Store Name"
|
||||
field :site_description, :string, default: "Discover unique products and original designs."
|
||||
field :logo_mode, :string, default: "text-only"
|
||||
field :logo_image_id, :binary_id
|
||||
field :logo_size, :integer, default: 36
|
||||
@ -68,8 +66,6 @@ defmodule Berrypod.Settings.ThemeSettings do
|
||||
:grid_columns,
|
||||
:header_layout,
|
||||
:accent_color,
|
||||
:site_name,
|
||||
:site_description,
|
||||
:logo_mode,
|
||||
:logo_image_id,
|
||||
:logo_size,
|
||||
|
||||
@ -3,7 +3,7 @@ defmodule Berrypod.Setup do
|
||||
Aggregates setup status checks for the setup flow and launch checklist.
|
||||
"""
|
||||
|
||||
alias Berrypod.{Accounts, Orders, Products, Settings}
|
||||
alias Berrypod.{Accounts, Mailer, Orders, Products, Settings, Shipping}
|
||||
|
||||
@setup_secret_key :berrypod_setup_secret
|
||||
|
||||
@ -62,10 +62,12 @@ defmodule Berrypod.Setup do
|
||||
## Launch checklist phase
|
||||
|
||||
* `products_synced` / `product_count` — products imported
|
||||
* `has_shipping` — at least one shipping rate exists
|
||||
* `theme_customised` — theme settings have been saved at least once
|
||||
* `has_orders` — at least one paid order exists
|
||||
* `email_configured` — email adapter configured and verified
|
||||
* `site_live` — shop is open to the public
|
||||
* `can_go_live` — minimum requirements met to go live
|
||||
* `can_go_live` — minimum requirements met to go live (includes shipping)
|
||||
* `checklist_dismissed` — admin has dismissed the launch checklist
|
||||
"""
|
||||
def setup_status do
|
||||
@ -77,6 +79,7 @@ defmodule Berrypod.Setup do
|
||||
stripe_connected = Settings.has_secret?("stripe_api_key")
|
||||
admin_created = Accounts.has_admin?()
|
||||
site_live = Settings.site_live?()
|
||||
has_shipping = Shipping.has_shipping_rates?()
|
||||
|
||||
%{
|
||||
# Setup phase
|
||||
@ -89,10 +92,12 @@ defmodule Berrypod.Setup do
|
||||
# Launch checklist
|
||||
products_synced: products_synced,
|
||||
product_count: product_count,
|
||||
has_shipping: has_shipping,
|
||||
theme_customised: Settings.get_setting("theme_customised", false) == true,
|
||||
has_orders: Orders.has_paid_orders?(),
|
||||
email_configured: Mailer.email_configured?(),
|
||||
site_live: site_live,
|
||||
can_go_live: provider_connected and products_synced and stripe_connected,
|
||||
can_go_live: provider_connected and products_synced and stripe_connected and has_shipping,
|
||||
checklist_dismissed: Settings.get_setting("checklist_dismissed", false) == true
|
||||
}
|
||||
end
|
||||
|
||||
@ -310,6 +310,15 @@ defmodule Berrypod.Shipping do
|
||||
# Queries
|
||||
# =============================================================================
|
||||
|
||||
@doc """
|
||||
Returns true if at least one shipping rate exists.
|
||||
"""
|
||||
def has_shipping_rates? do
|
||||
ShippingRate
|
||||
|> limit(1)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of distinct country codes that have shipping rates.
|
||||
|
||||
|
||||
@ -30,6 +30,8 @@ defmodule BerrypodWeb.AdminLayoutHook do
|
||||
|> assign(:site_live, Settings.site_live?())
|
||||
|> assign(:email_configured, Berrypod.Mailer.email_configured?())
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:site_name, Settings.site_name())
|
||||
|> assign(:site_description, Settings.site_description())
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> assign(:attention_count, ActivityLog.count_needing_attention())
|
||||
|> Phoenix.LiveView.attach_hook(:set_current_path, :handle_params, fn _params,
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
assigns[:page_description] || @theme_settings.site_description ||
|
||||
"Welcome to #{@theme_settings.site_name}"
|
||||
assigns[:page_description] || @site_description ||
|
||||
"Welcome to #{@site_name}"
|
||||
}
|
||||
/>
|
||||
<!-- Favicon & PWA -->
|
||||
@ -17,19 +17,19 @@
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content={@theme_settings.accent_color || "#000000"} />
|
||||
<.live_title suffix={" · #{@theme_settings.site_name}"}>
|
||||
<.live_title suffix={" · #{@site_name}"}>
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<% og_title =
|
||||
if assigns[:page_title],
|
||||
do: "#{assigns[:page_title]} · #{@theme_settings.site_name}",
|
||||
else: @theme_settings.site_name
|
||||
do: "#{assigns[:page_title]} · #{@site_name}",
|
||||
else: @site_name
|
||||
|
||||
og_description =
|
||||
assigns[:page_description] ||
|
||||
@theme_settings.site_description ||
|
||||
"Welcome to #{@theme_settings.site_name}" %>
|
||||
<meta property="og:site_name" content={@theme_settings.site_name} />
|
||||
@site_description ||
|
||||
"Welcome to #{@site_name}" %>
|
||||
<meta property="og:site_name" content={@site_name} />
|
||||
<meta property="og:title" content={og_title} />
|
||||
<meta property="og:description" content={og_description} />
|
||||
<meta property="og:type" content={assigns[:og_type] || "website"} />
|
||||
|
||||
@ -48,7 +48,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
# Keys accepted by shop_layout — used by layout_assigns/1 so page templates
|
||||
# can spread assigns without listing each one explicitly.
|
||||
@layout_keys ~w(theme_settings logo_image header_image mode cart_items cart_count
|
||||
@layout_keys ~w(theme_settings site_name logo_image header_image mode cart_items cart_count
|
||||
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
||||
search_query search_results search_open categories shipping_estimate
|
||||
country_code available_countries editing editor_current_path editor_sidebar_open
|
||||
@ -75,6 +75,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
The `error_page` flag disables the CartPersist hook and mobile bottom nav.
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :logo_image, :any, required: true
|
||||
attr :header_image, :any, required: true
|
||||
attr :mode, :atom, required: true
|
||||
@ -119,6 +120,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
<.shop_header
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
active_page={@active_page}
|
||||
@ -135,6 +137,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
<.shop_footer
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
mode={@mode}
|
||||
categories={assigns[:categories] || []}
|
||||
footer_nav_items={@footer_nav_items}
|
||||
@ -513,7 +516,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
## Attributes
|
||||
|
||||
* `theme_settings` - Required. The theme settings map containing site_name.
|
||||
* `theme_settings` - Required. The theme settings map.
|
||||
* `mode` - Optional. Either `:live` (default) for real navigation or
|
||||
`:preview` for theme preview mode with phx-click handlers.
|
||||
|
||||
@ -523,6 +526,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<.shop_footer theme_settings={@theme_settings} mode={:preview} />
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :mode, :atom, default: :live
|
||||
attr :categories, :list, default: []
|
||||
attr :footer_nav_items, :list, default: []
|
||||
@ -622,7 +626,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<!-- Bottom Bar -->
|
||||
<div class="footer-bottom">
|
||||
<p class="footer-copyright">
|
||||
© {@current_year} {@theme_settings.site_name}
|
||||
© {@current_year} {@site_name}
|
||||
</p>
|
||||
<.social_links />
|
||||
</div>
|
||||
@ -649,6 +653,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<.shop_header theme_settings={@theme_settings} mode={:preview} cart_count={2} />
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :header_image, :map, default: nil
|
||||
attr :active_page, :string, default: nil
|
||||
@ -670,6 +675,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<div class="shop-logo">
|
||||
<.logo_content
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
logo_image={@logo_image}
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
@ -790,6 +796,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
# Logo content that links to home, except when already on home page.
|
||||
# This follows accessibility best practices - current page should not be a link.
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :active_page, :string, default: nil
|
||||
attr :mode, :atom, default: :live
|
||||
@ -800,7 +807,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
|
||||
~H"""
|
||||
<%= if @is_home do %>
|
||||
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||
<.logo_inner theme_settings={@theme_settings} site_name={@site_name} logo_image={@logo_image} />
|
||||
<% else %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
@ -809,11 +816,19 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
phx-value-page="home"
|
||||
class="shop-logo-link"
|
||||
>
|
||||
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||
<.logo_inner
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
logo_image={@logo_image}
|
||||
/>
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate="/" class="shop-logo-link">
|
||||
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
|
||||
<.logo_inner
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
logo_image={@logo_image}
|
||||
/>
|
||||
</.link>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@ -821,6 +836,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
end
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :logo_image, :map, default: nil
|
||||
|
||||
defp logo_inner(assigns) do
|
||||
@ -828,36 +844,36 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
<%= case @theme_settings.logo_mode do %>
|
||||
<% "text-only" -> %>
|
||||
<span class="shop-logo-text">
|
||||
{@theme_settings.site_name}
|
||||
{@site_name}
|
||||
</span>
|
||||
<% "logo-text" -> %>
|
||||
<%= if @logo_image do %>
|
||||
<img
|
||||
src={logo_url(@logo_image, @theme_settings)}
|
||||
alt={@theme_settings.site_name}
|
||||
alt={@site_name}
|
||||
class="shop-logo-img"
|
||||
style={"height: #{@theme_settings.logo_size}px;"}
|
||||
/>
|
||||
<% end %>
|
||||
<span class="shop-logo-text">
|
||||
{@theme_settings.site_name}
|
||||
{@site_name}
|
||||
</span>
|
||||
<% "logo-only" -> %>
|
||||
<%= if @logo_image do %>
|
||||
<img
|
||||
src={logo_url(@logo_image, @theme_settings)}
|
||||
alt={@theme_settings.site_name}
|
||||
alt={@site_name}
|
||||
class="shop-logo-img"
|
||||
style={"height: #{@theme_settings.logo_size}px;"}
|
||||
/>
|
||||
<% else %>
|
||||
<span class="shop-logo-text">
|
||||
{@theme_settings.site_name}
|
||||
{@site_name}
|
||||
</span>
|
||||
<% end %>
|
||||
<% _ -> %>
|
||||
<span class="shop-logo-text">
|
||||
{@theme_settings.site_name}
|
||||
{@site_name}
|
||||
</span>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
@ -85,6 +85,8 @@ defmodule BerrypodWeb.ErrorHTML do
|
||||
assigns =
|
||||
assigns
|
||||
|> Map.put(:theme_settings, theme_settings)
|
||||
|> Map.put(:site_name, safe_load(&Settings.site_name/0) || "Store Name")
|
||||
|> Map.put(:site_description, safe_load(&Settings.site_description/0) || "")
|
||||
|> Map.put(:generated_css, generated_css)
|
||||
|> Map.put(:logo_image, logo_image)
|
||||
|> Map.put(:header_image, header_image)
|
||||
|
||||
@ -25,15 +25,16 @@ defmodule BerrypodWeb.FaviconController do
|
||||
|
||||
def webmanifest(conn, _params) do
|
||||
settings = Settings.get_theme_settings()
|
||||
site_name = Settings.site_name()
|
||||
|
||||
short_name =
|
||||
case settings.favicon_short_name do
|
||||
name when is_binary(name) and name != "" -> name
|
||||
_ -> String.slice(settings.site_name, 0, 12)
|
||||
_ -> String.slice(site_name, 0, 12)
|
||||
end
|
||||
|
||||
manifest = %{
|
||||
name: settings.site_name,
|
||||
name: site_name,
|
||||
short_name: short_name,
|
||||
theme_color: settings.accent_color || "#000000",
|
||||
background_color: settings.icon_background_color || "#ffffff",
|
||||
|
||||
@ -3,14 +3,6 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
|
||||
alias Berrypod.{Cart, Orders, Products, Settings}
|
||||
|
||||
@checklist_items [
|
||||
%{key: :products_synced, label: "Sync your products", href: "/admin/providers"},
|
||||
%{key: :stripe_connected, label: "Connect Stripe", href: "/admin/settings"},
|
||||
%{key: :theme_customised, label: "Customise your theme", href: "/admin/theme"},
|
||||
%{key: :has_orders, label: "Place a test order", href: "/"},
|
||||
%{key: :site_live, label: "Go live", href: nil}
|
||||
]
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
setup = Berrypod.Setup.setup_status()
|
||||
@ -23,6 +15,7 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
|> assign(:page_title, "Dashboard")
|
||||
|> assign(:setup, setup)
|
||||
|> assign(:show_checklist, show_checklist?(setup))
|
||||
|> assign(:checklist_collapsed, false)
|
||||
|> assign(:just_went_live, false)
|
||||
|> assign(:paid_count, paid_count)
|
||||
|> assign(:revenue, Orders.total_revenue())
|
||||
@ -43,14 +36,8 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
|> assign(:just_went_live, true)}
|
||||
end
|
||||
|
||||
def handle_event("dismiss_checklist", _params, socket) do
|
||||
{:ok, _} = Settings.put_setting("checklist_dismissed", true, "boolean")
|
||||
setup = %{socket.assigns.setup | checklist_dismissed: true}
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:setup, setup)
|
||||
|> assign(:show_checklist, false)}
|
||||
def handle_event("toggle_checklist", _params, socket) do
|
||||
{:noreply, assign(socket, :checklist_collapsed, !socket.assigns.checklist_collapsed)}
|
||||
end
|
||||
|
||||
# ── Render ──
|
||||
@ -78,7 +65,11 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
</div>
|
||||
|
||||
<%!-- Launch checklist --%>
|
||||
<.launch_checklist :if={@show_checklist and !@just_went_live} setup={@setup} />
|
||||
<.launch_checklist
|
||||
:if={@show_checklist and !@just_went_live}
|
||||
setup={@setup}
|
||||
collapsed={@checklist_collapsed}
|
||||
/>
|
||||
|
||||
<%!-- Stats --%>
|
||||
<div class="admin-stats-grid">
|
||||
@ -158,86 +149,147 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
# ==========================================================================
|
||||
|
||||
attr :setup, :map, required: true
|
||||
attr :collapsed, :boolean, required: true
|
||||
|
||||
defp launch_checklist(assigns) do
|
||||
items =
|
||||
Enum.map(@checklist_items, fn item ->
|
||||
Map.put(item, :done, Map.get(assigns.setup, item.key, false))
|
||||
end)
|
||||
items = checklist_items(assigns.setup)
|
||||
|
||||
done_count = Enum.count(items, & &1.done)
|
||||
total = length(items)
|
||||
# Email is optional — exclude from progress count
|
||||
required_items = Enum.reject(items, &(&1.key == :email_configured))
|
||||
done_count = Enum.count(required_items, & &1.done)
|
||||
total = length(required_items)
|
||||
progress_pct = round(done_count / total * 100)
|
||||
|
||||
can_go_live =
|
||||
assigns.setup.provider_connected and assigns.setup.products_synced and
|
||||
assigns.setup.stripe_connected
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:items, items)
|
||||
|> assign(:done_count, done_count)
|
||||
|> assign(:total, total)
|
||||
|> assign(:progress_pct, progress_pct)
|
||||
|> assign(:can_go_live, can_go_live)
|
||||
|> assign(:can_go_live, assigns.setup.can_go_live)
|
||||
|> assign(:has_shipping, assigns.setup.has_shipping)
|
||||
|
||||
~H"""
|
||||
<div class="admin-checklist admin-card-spaced">
|
||||
<div class="admin-checklist-header">
|
||||
<button type="button" phx-click="toggle_checklist" class="admin-checklist-header">
|
||||
<h2 class="admin-checklist-title">Launch checklist</h2>
|
||||
<div class="admin-checklist-progress">
|
||||
<span>{@done_count} of {@total}</span>
|
||||
<div class="admin-checklist-bar">
|
||||
<div class="admin-checklist-bar-fill" style={"width: #{@progress_pct}%"} />
|
||||
</div>
|
||||
<.icon
|
||||
name={if @collapsed, do: "hero-chevron-down-mini", else: "hero-chevron-up-mini"}
|
||||
class="size-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ul class="admin-checklist-items">
|
||||
<ul :if={!@collapsed} class="admin-checklist-items">
|
||||
<li :for={item <- @items} class="admin-checklist-item">
|
||||
<span class={["admin-checklist-check", item.done && "admin-checklist-check-done"]}>
|
||||
<.icon :if={item.done} name="hero-check-mini" class="size-3" />
|
||||
</span>
|
||||
|
||||
<span class={["admin-checklist-label", item.done && "admin-checklist-label-done"]}>
|
||||
{item.label}
|
||||
</span>
|
||||
<div class="admin-checklist-content">
|
||||
<div class="admin-checklist-row">
|
||||
<span class={[
|
||||
"admin-checklist-label",
|
||||
item.done && "admin-checklist-label-done"
|
||||
]}>
|
||||
{item.label}
|
||||
<span :if={item[:optional]} class="admin-checklist-optional">optional</span>
|
||||
</span>
|
||||
|
||||
<span class="admin-checklist-action">
|
||||
<%= if item.key == :site_live do %>
|
||||
<button
|
||||
phx-click="go_live"
|
||||
disabled={!@can_go_live}
|
||||
class="admin-btn admin-btn-primary admin-btn-sm"
|
||||
>
|
||||
<.icon name="hero-rocket-launch-mini" class="size-4" /> Go live
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
:if={!item.done}
|
||||
navigate={item.href}
|
||||
class="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
>
|
||||
{if item.done, do: "View", else: "Start"} →
|
||||
</.link>
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="admin-checklist-action">
|
||||
<%= if item.key == :site_live do %>
|
||||
<button
|
||||
phx-click="go_live"
|
||||
disabled={!@can_go_live}
|
||||
class="admin-btn admin-btn-primary admin-btn-sm"
|
||||
>
|
||||
<.icon name="hero-rocket-launch-mini" class="size-4" /> Go live
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
:if={!item.done}
|
||||
navigate={item.href}
|
||||
class="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
>
|
||||
Start →
|
||||
</.link>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p :if={item[:hint] && !item.done} class="admin-checklist-hint">
|
||||
{item.hint}
|
||||
</p>
|
||||
|
||||
<p
|
||||
:if={item.key == :site_live && !@can_go_live && !@has_shipping}
|
||||
class="admin-checklist-hint"
|
||||
>
|
||||
Shipping rates haven't synced yet. Try re-syncing your products from the <.link
|
||||
navigate="/admin/providers"
|
||||
class="admin-link"
|
||||
>providers page</.link>.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="admin-checklist-footer">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="dismiss_checklist"
|
||||
class="admin-btn admin-btn-ghost admin-btn-sm"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp checklist_items(setup) do
|
||||
[
|
||||
# Setup wizard items (done first during onboarding)
|
||||
%{
|
||||
key: :provider_connected,
|
||||
label: "Connect a print provider",
|
||||
href: "/admin/providers"
|
||||
},
|
||||
%{
|
||||
key: :stripe_connected,
|
||||
label: "Connect Stripe",
|
||||
href: "/admin/settings"
|
||||
},
|
||||
# Post-setup items
|
||||
%{
|
||||
key: :products_synced,
|
||||
label: "Sync your products",
|
||||
href: if(setup.provider_connected, do: "/admin/products", else: "/admin/providers"),
|
||||
hint: "Import products from your print provider."
|
||||
},
|
||||
%{
|
||||
key: :theme_customised,
|
||||
label: "Customise your theme",
|
||||
href: "/admin/theme",
|
||||
hint: "Upload your logo, pick your colours, and choose a font that matches your brand."
|
||||
},
|
||||
%{
|
||||
key: :email_configured,
|
||||
label: "Set up email",
|
||||
href: "/admin/settings",
|
||||
optional: true,
|
||||
hint: "Needed for order confirmations, abandoned cart emails, and the contact form."
|
||||
},
|
||||
%{
|
||||
key: :has_orders,
|
||||
label: "Place a test order",
|
||||
href: "/",
|
||||
hint:
|
||||
"Use card 4242 4242 4242 4242 with any future expiry and CVC. " <>
|
||||
"You'll see the order in Orders when it works."
|
||||
},
|
||||
%{key: :site_live, label: "Go live"}
|
||||
]
|
||||
|> Enum.map(fn item ->
|
||||
Map.put(item, :done, Map.get(setup, item.key, false))
|
||||
end)
|
||||
end
|
||||
|
||||
# ==========================================================================
|
||||
# Components
|
||||
# ==========================================================================
|
||||
@ -287,7 +339,7 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
||||
# ==========================================================================
|
||||
|
||||
defp show_checklist?(setup) do
|
||||
not setup.site_live and not setup.checklist_dismissed
|
||||
not setup.site_live
|
||||
end
|
||||
|
||||
defp format_revenue(amount_pence) when is_integer(amount_pence) do
|
||||
|
||||
@ -688,6 +688,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
page_data={@page_data}
|
||||
preview_data={@preview_data}
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
generated_css={@generated_css}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
|
||||
@ -26,6 +26,8 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:site_name, Settings.site_name())
|
||||
|> assign(:site_description, Settings.site_description())
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> assign(:preview_page, :home)
|
||||
|> assign(:presets_with_descriptions, Presets.all_with_descriptions())
|
||||
@ -174,6 +176,16 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Settings stored outside the theme JSON
|
||||
@standalone_settings ~w(site_name site_description)
|
||||
|
||||
@impl true
|
||||
def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket)
|
||||
when field in @standalone_settings do
|
||||
Settings.put_setting(field, value, "string")
|
||||
{:noreply, assign(socket, String.to_existing_atom(field), value)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket) do
|
||||
field_atom = String.to_existing_atom(field)
|
||||
@ -197,6 +209,19 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_setting", %{"field" => field} = params, socket)
|
||||
when field in @standalone_settings do
|
||||
value = params[field]
|
||||
|
||||
if value do
|
||||
Settings.put_setting(field, value, "string")
|
||||
{:noreply, assign(socket, String.to_existing_atom(field), value)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_setting", %{"field" => field} = params, socket) do
|
||||
# For phx-change events from select/input elements, the value comes from the name attribute
|
||||
@ -439,6 +464,7 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
||||
attr :page, :atom, required: true
|
||||
attr :preview_data, :map, required: true
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, required: true
|
||||
attr :logo_image, :any, required: true
|
||||
attr :header_image, :any, required: true
|
||||
attr :cart_drawer_open, :boolean, default: false
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="site_name"
|
||||
value={@theme_settings.site_name}
|
||||
value={@site_name}
|
||||
placeholder="Your shop name"
|
||||
class="admin-input admin-input-lg"
|
||||
/>
|
||||
@ -374,7 +374,7 @@
|
||||
type="text"
|
||||
name="favicon_short_name"
|
||||
value={@theme_settings.favicon_short_name}
|
||||
placeholder={String.slice(@theme_settings.site_name, 0, 12)}
|
||||
placeholder={String.slice(@site_name, 0, 12)}
|
||||
maxlength="12"
|
||||
class="admin-input admin-input-sm"
|
||||
/>
|
||||
@ -1181,7 +1181,7 @@
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
<span class="theme-browser-url-text truncate">
|
||||
{@theme_settings.site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
|
||||
{@site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -1213,6 +1213,7 @@
|
||||
page={@preview_page}
|
||||
preview_data={@preview_data}
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
cart_drawer_open={@cart_drawer_open}
|
||||
|
||||
@ -2,7 +2,6 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.{Accounts, Products, Settings, Setup}
|
||||
alias Berrypod.Products.ProviderConnection
|
||||
alias Berrypod.Providers.Provider
|
||||
alias Berrypod.Stripe.Setup, as: StripeSetup
|
||||
|
||||
@ -38,25 +37,30 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
logged_in? = get_user(socket) != nil
|
||||
provider_conn = Products.get_first_provider_connection()
|
||||
|
||||
current_step =
|
||||
cond do
|
||||
not logged_in? -> 1
|
||||
not setup.provider_connected -> 2
|
||||
true -> 3
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "Set up your shop")
|
||||
|> assign(:setup, setup)
|
||||
|> assign(:logged_in?, logged_in?)
|
||||
|> assign(:current_step, current_step)
|
||||
# Secret gate
|
||||
|> assign(:require_secret?, Setup.require_setup_secret?())
|
||||
|> assign(:secret_verified, false)
|
||||
|> assign(:secret_form, to_form(%{"secret" => ""}, as: :secret))
|
||||
# Account (card 1)
|
||||
|> assign(:account_form, to_form(%{"email" => ""}, as: :account))
|
||||
|> assign(:account_form, to_form(%{"email" => "", "shop_name" => ""}, as: :account))
|
||||
# Provider (card 2)
|
||||
|> assign(:providers, Provider.all())
|
||||
|> assign(:selected_provider, nil)
|
||||
|> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider))
|
||||
|> assign(:provider_testing, false)
|
||||
|> assign(:provider_test_result, nil)
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_conn, provider_conn)
|
||||
|> assign(:pending_provider_key, nil)
|
||||
# Stripe (card 3)
|
||||
|> assign(:stripe_form, to_form(%{"api_key" => ""}, as: :stripe))
|
||||
|> assign(:stripe_connecting, false)
|
||||
@ -75,10 +79,17 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
|
||||
# ── Events: Account ──
|
||||
|
||||
def handle_event("create_account", %{"account" => %{"email" => email}}, socket) do
|
||||
def handle_event("create_account", %{"account" => params}, socket) do
|
||||
email = params["email"]
|
||||
shop_name = String.trim(params["shop_name"] || "")
|
||||
|
||||
if email == "" do
|
||||
{:noreply, put_flash(socket, :error, "Please enter your email address")}
|
||||
else
|
||||
if shop_name != "" do
|
||||
Settings.put_setting("site_name", shop_name, "string")
|
||||
end
|
||||
|
||||
case Accounts.register_and_confirm_admin(%{email: email}) do
|
||||
{:ok, user} ->
|
||||
token = Accounts.generate_login_token(user)
|
||||
@ -105,33 +116,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_provider, type)
|
||||
|> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider))
|
||||
|> assign(:provider_test_result, nil)
|
||||
|> assign(:pending_provider_key, nil)}
|
||||
end
|
||||
|
||||
def handle_event("validate_provider", %{"provider" => params}, socket) do
|
||||
{:noreply, assign(socket, pending_provider_key: params["api_key"])}
|
||||
end
|
||||
|
||||
def handle_event("test_provider", _params, socket) do
|
||||
type = socket.assigns.selected_provider
|
||||
api_key = socket.assigns.pending_provider_key
|
||||
|
||||
if api_key in [nil, ""] do
|
||||
{:noreply, assign(socket, provider_test_result: {:error, :no_api_key})}
|
||||
else
|
||||
socket = assign(socket, provider_testing: true, provider_test_result: nil)
|
||||
|
||||
temp_conn = %ProviderConnection{
|
||||
provider_type: type,
|
||||
api_key_encrypted: encrypt_api_key(api_key)
|
||||
}
|
||||
|
||||
result = Berrypod.Providers.test_connection(temp_conn)
|
||||
|
||||
{:noreply, assign(socket, provider_testing: false, provider_test_result: result)}
|
||||
end
|
||||
|> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider))}
|
||||
end
|
||||
|
||||
def handle_event("connect_provider", %{"provider" => %{"api_key" => api_key}}, socket) do
|
||||
@ -142,22 +127,32 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
else
|
||||
socket = assign(socket, provider_connecting: true)
|
||||
|
||||
params =
|
||||
%{"api_key" => api_key, "provider_type" => type}
|
||||
|> maybe_add_shop_config(socket.assigns.provider_test_result)
|
||||
|> maybe_add_name(socket.assigns.provider_test_result, type)
|
||||
name =
|
||||
case Provider.get(type) do
|
||||
nil -> type
|
||||
info -> info.name
|
||||
end
|
||||
|
||||
params = %{"api_key" => api_key, "provider_type" => type, "name" => name}
|
||||
|
||||
case Products.create_provider_connection(params) do
|
||||
{:ok, connection} ->
|
||||
Products.enqueue_sync(connection)
|
||||
setup = Setup.setup_status()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_conn, connection)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_conn, connection)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
||||
end
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply,
|
||||
@ -180,11 +175,18 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
{:ok, _result} ->
|
||||
setup = Setup.setup_status()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stripe_connecting, false)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Stripe connected")}
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stripe_connecting, false)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Stripe connected")}
|
||||
end
|
||||
|
||||
{:error, message} ->
|
||||
{:noreply,
|
||||
@ -243,20 +245,27 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
<%!-- All three setup cards --%>
|
||||
<div class="setup-sections">
|
||||
<.section_card
|
||||
title="Create admin account"
|
||||
title="Set up your account"
|
||||
number={1}
|
||||
done={@logged_in?}
|
||||
summary={account_summary(assigns)}
|
||||
>
|
||||
<p class="setup-hint">Enter your email to create the admin account.</p>
|
||||
<p class="setup-hint">Name your shop and create the admin account.</p>
|
||||
<.form for={@account_form} phx-submit="create_account">
|
||||
<.input
|
||||
field={@account_form[:shop_name]}
|
||||
type="text"
|
||||
label="Shop name"
|
||||
placeholder="e.g. Acme Prints"
|
||||
autocomplete="off"
|
||||
phx-mounted={@current_step == 1 && JS.focus()}
|
||||
/>
|
||||
<.input
|
||||
field={@account_form[:email]}
|
||||
type="email"
|
||||
label="Email address"
|
||||
autocomplete="email"
|
||||
required
|
||||
phx-mounted={JS.focus()}
|
||||
/>
|
||||
<div class="setup-actions">
|
||||
<.button phx-disable-with="Creating account...">Create account</.button>
|
||||
@ -274,8 +283,6 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
providers={@providers}
|
||||
selected={@selected_provider}
|
||||
form={@provider_form}
|
||||
testing={@provider_testing}
|
||||
test_result={@provider_test_result}
|
||||
connecting={@provider_connecting}
|
||||
/>
|
||||
</.section_card>
|
||||
@ -289,6 +296,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
<.stripe_section
|
||||
form={@stripe_form}
|
||||
connecting={@stripe_connecting}
|
||||
focus={@current_step == 3}
|
||||
/>
|
||||
</.section_card>
|
||||
</div>
|
||||
@ -346,8 +354,6 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
attr :providers, :list, required: true
|
||||
attr :selected, :string, default: nil
|
||||
attr :form, :any, required: true
|
||||
attr :testing, :boolean, required: true
|
||||
attr :test_result, :any, default: nil
|
||||
attr :connecting, :boolean, required: true
|
||||
|
||||
defp provider_section(assigns) do
|
||||
@ -376,7 +382,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<.form for={@form} phx-change="validate_provider" phx-submit="connect_provider">
|
||||
<.form for={@form} phx-submit="connect_provider">
|
||||
<.input
|
||||
field={@form[:api_key]}
|
||||
type="password"
|
||||
@ -386,58 +392,21 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
/>
|
||||
|
||||
<div class="setup-actions">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="test_provider"
|
||||
disabled={@testing}
|
||||
class="admin-btn admin-btn-secondary"
|
||||
>
|
||||
<%= if @testing do %>
|
||||
<.icon name="hero-arrow-path" class="size-4 animate-spin" /> Checking...
|
||||
<% else %>
|
||||
<.icon name="hero-signal" class="size-4" /> Check connection
|
||||
<% end %>
|
||||
</button>
|
||||
<.button type="submit" disabled={@connecting or @testing}>
|
||||
<.button type="submit" disabled={@connecting}>
|
||||
{if @connecting, do: "Connecting...", else: "Connect"}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<.provider_test_feedback :if={@test_result} result={@test_result} />
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :result, :any, required: true
|
||||
|
||||
defp provider_test_feedback(assigns) do
|
||||
~H"""
|
||||
<div class="setup-test-result">
|
||||
<%= case @result do %>
|
||||
<% {:ok, info} -> %>
|
||||
<span class="setup-test-ok">
|
||||
<.icon name="hero-check-circle" class="size-4" />
|
||||
Connected{if info[:shop_name], do: " to #{info.shop_name}", else: ""}
|
||||
</span>
|
||||
<% {:error, :no_api_key} -> %>
|
||||
<span class="setup-test-error">
|
||||
<.icon name="hero-x-circle" class="size-4" /> Please enter your API token
|
||||
</span>
|
||||
<% {:error, reason} -> %>
|
||||
<span class="setup-test-error">
|
||||
<.icon name="hero-x-circle" class="size-4" /> {format_error(reason)}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Stripe section ──
|
||||
|
||||
attr :form, :any, required: true
|
||||
attr :connecting, :boolean, required: true
|
||||
attr :focus, :boolean, default: false
|
||||
|
||||
defp stripe_section(assigns) do
|
||||
~H"""
|
||||
@ -461,13 +430,14 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
label="Secret key"
|
||||
autocomplete="off"
|
||||
placeholder="sk_test_... or sk_live_..."
|
||||
phx-mounted={@focus && JS.focus()}
|
||||
/>
|
||||
<p class="setup-key-hint">
|
||||
Starts with <code>sk_test_</code> or <code>sk_live_</code>. Encrypted at rest.
|
||||
</p>
|
||||
<div class="setup-actions">
|
||||
<.button phx-disable-with="Connecting...">
|
||||
{if @connecting, do: "Connecting...", else: "Connect Stripe"}
|
||||
{if @connecting, do: "Connecting...", else: "Connect"}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
@ -501,31 +471,6 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
|
||||
defp stripe_summary(_), do: nil
|
||||
|
||||
defp encrypt_api_key(api_key) do
|
||||
case Berrypod.Vault.encrypt(api_key) do
|
||||
{:ok, encrypted} -> encrypted
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_shop_config(params, {: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: name}}, _type) when is_binary(name) do
|
||||
Map.put_new(params, "name", name)
|
||||
end
|
||||
|
||||
defp maybe_add_name(params, _, type) do
|
||||
case Provider.get(type) do
|
||||
nil -> Map.put_new(params, "name", type)
|
||||
info -> Map.put_new(params, "name", info.name)
|
||||
end
|
||||
end
|
||||
|
||||
defp provider_card_options(providers) do
|
||||
Enum.map(providers, fn provider ->
|
||||
option = %{
|
||||
@ -541,11 +486,4 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_error(:unauthorized), do: "That token doesn't seem to be valid"
|
||||
defp format_error(:timeout), do: "Couldn't reach the provider — try again"
|
||||
defp format_error(:provider_not_implemented), do: "This provider isn't supported yet"
|
||||
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 token and try again"
|
||||
end
|
||||
|
||||
@ -12,9 +12,9 @@ defmodule BerrypodWeb.Shop.ComingSoon do
|
||||
<main class="coming-soon" role="main">
|
||||
<div>
|
||||
<div :if={@logo_image} class="coming-soon-logo">
|
||||
<img src={logo_url(@logo_image)} alt={@theme_settings.site_name} />
|
||||
<img src={logo_url(@logo_image)} alt={@site_name} />
|
||||
</div>
|
||||
<h1 class="coming-soon-title">{@theme_settings.site_name}</h1>
|
||||
<h1 class="coming-soon-title">{@site_name}</h1>
|
||||
<p class="coming-soon-message">
|
||||
We're getting things ready. Check back soon.
|
||||
</p>
|
||||
|
||||
@ -9,7 +9,7 @@ defmodule BerrypodWeb.Shop.Home do
|
||||
extra = Pages.load_block_data(page.blocks, socket.assigns)
|
||||
|
||||
base = BerrypodWeb.Endpoint.url()
|
||||
site_name = socket.assigns.theme_settings.site_name
|
||||
site_name = socket.assigns.site_name
|
||||
|
||||
org_ld =
|
||||
Jason.encode!(
|
||||
|
||||
@ -34,6 +34,8 @@ defmodule BerrypodWeb.Plugs.LoadTheme do
|
||||
|
||||
conn
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:site_name, Settings.site_name())
|
||||
|> assign(:site_description, Settings.site_description())
|
||||
|> assign(:generated_css, generated_css)
|
||||
end
|
||||
end
|
||||
|
||||
@ -56,6 +56,8 @@ defmodule BerrypodWeb.ThemeHook do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:site_name, Settings.site_name())
|
||||
|> assign(:site_description, Settings.site_description())
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> assign(:logo_image, Media.get_logo())
|
||||
|> assign(:header_image, Media.get_header())
|
||||
|
||||
@ -20,6 +20,7 @@ defmodule Berrypod.SetupTest do
|
||||
refute status.can_go_live
|
||||
refute status.theme_customised
|
||||
refute status.has_orders
|
||||
refute status.has_shipping
|
||||
refute status.checklist_dismissed
|
||||
end
|
||||
|
||||
@ -89,7 +90,7 @@ defmodule Berrypod.SetupTest do
|
||||
assert Setup.setup_status().setup_complete
|
||||
end
|
||||
|
||||
test "can_go_live requires provider, products, and stripe" do
|
||||
test "can_go_live requires provider, products, stripe, and shipping" do
|
||||
{:ok, conn} =
|
||||
Products.create_provider_connection(%{
|
||||
name: "Test",
|
||||
@ -105,12 +106,25 @@ defmodule Berrypod.SetupTest do
|
||||
status: "active"
|
||||
})
|
||||
|
||||
# Still missing stripe
|
||||
# Still missing stripe and shipping
|
||||
refute Setup.setup_status().can_go_live
|
||||
|
||||
# Add stripe
|
||||
{:ok, _} = Settings.put_secret("stripe_api_key", "sk_test_abc123")
|
||||
|
||||
# Still missing shipping
|
||||
refute Setup.setup_status().can_go_live
|
||||
|
||||
Berrypod.Shipping.upsert_rates(conn.id, [
|
||||
%{
|
||||
blueprint_id: 1,
|
||||
print_provider_id: 1,
|
||||
country_code: "GB",
|
||||
first_item_cost: 350,
|
||||
additional_item_cost: 200,
|
||||
currency: "GBP"
|
||||
}
|
||||
])
|
||||
|
||||
assert Setup.setup_status().can_go_live
|
||||
end
|
||||
|
||||
@ -121,5 +135,64 @@ defmodule Berrypod.SetupTest do
|
||||
|
||||
assert Setup.setup_status().theme_customised
|
||||
end
|
||||
|
||||
test "detects shipping rates" do
|
||||
refute Setup.setup_status().has_shipping
|
||||
|
||||
{:ok, conn} =
|
||||
Products.create_provider_connection(%{
|
||||
name: "Test",
|
||||
provider_type: "printify",
|
||||
api_key: "test_api_key"
|
||||
})
|
||||
|
||||
Berrypod.Shipping.upsert_rates(conn.id, [
|
||||
%{
|
||||
blueprint_id: 1,
|
||||
print_provider_id: 1,
|
||||
country_code: "GB",
|
||||
first_item_cost: 350,
|
||||
additional_item_cost: 200,
|
||||
currency: "GBP"
|
||||
}
|
||||
])
|
||||
|
||||
assert Setup.setup_status().has_shipping
|
||||
end
|
||||
|
||||
test "can_go_live requires shipping rates" do
|
||||
{:ok, conn} =
|
||||
Products.create_provider_connection(%{
|
||||
name: "Test",
|
||||
provider_type: "printify",
|
||||
api_key: "test_api_key"
|
||||
})
|
||||
|
||||
{:ok, _product} =
|
||||
Products.create_product(%{
|
||||
title: "Test product",
|
||||
provider_product_id: "ext-1",
|
||||
provider_connection_id: conn.id,
|
||||
status: "active"
|
||||
})
|
||||
|
||||
{:ok, _} = Settings.put_secret("stripe_api_key", "sk_test_abc123")
|
||||
|
||||
# No shipping rates yet
|
||||
refute Setup.setup_status().can_go_live
|
||||
|
||||
Berrypod.Shipping.upsert_rates(conn.id, [
|
||||
%{
|
||||
blueprint_id: 1,
|
||||
print_provider_id: 1,
|
||||
country_code: "GB",
|
||||
first_item_cost: 350,
|
||||
additional_item_cost: 200,
|
||||
currency: "GBP"
|
||||
}
|
||||
])
|
||||
|
||||
assert Setup.setup_status().can_go_live
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -27,6 +27,8 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin")
|
||||
|
||||
assert html =~ "Launch checklist"
|
||||
assert html =~ "Connect a print provider"
|
||||
assert html =~ "Connect Stripe"
|
||||
assert html =~ "Sync your products"
|
||||
assert html =~ "Customise your theme"
|
||||
assert html =~ "Go live"
|
||||
@ -39,17 +41,22 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
||||
refute html =~ "Launch checklist"
|
||||
end
|
||||
|
||||
test "dismiss checklist hides it", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin")
|
||||
assert has_element?(view, "button", "Dismiss")
|
||||
test "collapse and expand checklist", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, ~p"/admin")
|
||||
assert html =~ "Sync your products"
|
||||
|
||||
html = render_click(view, "dismiss_checklist")
|
||||
# Collapse
|
||||
html = render_click(view, "toggle_checklist")
|
||||
refute html =~ "Sync your products"
|
||||
assert html =~ "Launch checklist"
|
||||
|
||||
refute html =~ "Launch checklist"
|
||||
# Expand
|
||||
html = render_click(view, "toggle_checklist")
|
||||
assert html =~ "Sync your products"
|
||||
end
|
||||
|
||||
test "go live button works", %{conn: conn} do
|
||||
# Need provider + products + stripe for go live to be enabled
|
||||
# Need provider + products + stripe + shipping for go live
|
||||
{:ok, conn_record} =
|
||||
Berrypod.Products.create_provider_connection(%{
|
||||
name: "Test",
|
||||
@ -67,6 +74,17 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
||||
|
||||
{:ok, _} = Berrypod.Settings.put_secret("stripe_api_key", "sk_test_123")
|
||||
|
||||
Berrypod.Shipping.upsert_rates(conn_record.id, [
|
||||
%{
|
||||
blueprint_id: 1,
|
||||
print_provider_id: 1,
|
||||
country_code: "GB",
|
||||
first_item_cost: 350,
|
||||
additional_item_cost: 200,
|
||||
currency: "GBP"
|
||||
}
|
||||
])
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin")
|
||||
|
||||
html = render_click(view, "go_live")
|
||||
@ -74,6 +92,19 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
||||
assert html =~ "Your shop is live"
|
||||
assert Berrypod.Settings.site_live?()
|
||||
end
|
||||
|
||||
test "shows test order guidance", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin")
|
||||
|
||||
assert html =~ "4242 4242 4242 4242"
|
||||
end
|
||||
|
||||
test "shows email setup as optional", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin")
|
||||
|
||||
assert html =~ "Set up email"
|
||||
assert html =~ "optional"
|
||||
end
|
||||
end
|
||||
|
||||
describe "stats" do
|
||||
|
||||
@ -10,7 +10,7 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
||||
test "accessible on fresh install (no admin)", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
||||
assert html =~ "Set up your shop"
|
||||
assert html =~ "Create admin account"
|
||||
assert html =~ "Set up your account"
|
||||
end
|
||||
|
||||
test "redirects to /admin when setup is complete", %{conn: conn} do
|
||||
@ -60,7 +60,7 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
||||
test "shows all three cards with email form active", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
||||
|
||||
assert html =~ "Create admin account"
|
||||
assert html =~ "Set up your account"
|
||||
assert html =~ "Connect a print provider"
|
||||
assert html =~ "Connect payments"
|
||||
end
|
||||
@ -114,7 +114,7 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
||||
|
||||
assert html =~ "Secret key"
|
||||
assert html =~ "Connect Stripe"
|
||||
assert html =~ "Connect payments"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ defmodule BerrypodWeb.Shop.ComingSoonTest do
|
||||
end
|
||||
|
||||
test "displays the shop name", %{conn: conn} do
|
||||
{:ok, _} = Settings.update_theme_settings(%{site_name: "My Test Shop"})
|
||||
Settings.put_setting("site_name", "My Test Shop", "string")
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/coming-soon")
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ defmodule BerrypodWeb.PageRendererTest do
|
||||
%{
|
||||
__changed__: nil,
|
||||
theme_settings: %ThemeSettings{},
|
||||
site_name: "Test Store",
|
||||
logo_image: nil,
|
||||
header_image: nil,
|
||||
mode: :shop,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user