add setup foundations: site gate, registration lockdown, coming soon page
- Settings.site_live?/0 and set_site_live/1 for shop visibility control - Accounts.has_admin?/0 to detect single-tenant admin existence - Registration lockdown: /users/register redirects when admin exists - Setup.setup_status/0 aggregates provider, product, and stripe checks - Coming soon page at /coming-soon with themed styling - ThemeHook :require_site_live gate on all public shop routes - Site live → everyone through - Authenticated → admin preview through - No admin → fresh install demo through - Otherwise → redirect to coming soon - Go live / take offline toggle on /admin/settings - 648 tests, 0 failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
093bdcc7a6
commit
e64bf40a71
97
PROGRESS.md
97
PROGRESS.md
@ -20,73 +20,50 @@
|
|||||||
- Transactional emails (order confirmation, shipping notification)
|
- Transactional emails (order confirmation, shipping notification)
|
||||||
- Demo content polished and ready for production
|
- Demo content polished and ready for production
|
||||||
|
|
||||||
**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes mostly done (15/18). Next up: setup wizard, then real product data, then shipping costs.
|
**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes 15/18 done (remaining 3 are now tracked as features below).
|
||||||
|
|
||||||
## Next Up
|
## Task list
|
||||||
|
|
||||||
Two parallel tracks to make the shop fully functional:
|
Ordered by dependency level — admin shell chain first (unblocks most downstream work).
|
||||||
|
|
||||||
**Track A — Admin redesign** (can start now):
|
Plans: [admin-redesign.md](docs/plans/admin-redesign.md) | [setup-wizard.md](docs/plans/setup-wizard.md) | [search.md](docs/plans/search.md)
|
||||||
1. **Admin shell & dashboard** — sidebar nav, admin root layout, dashboard landing page. See [docs/plans/admin-redesign.md](docs/plans/admin-redesign.md)
|
|
||||||
2. **Custom admin CSS** — replace DaisyUI with lean admin stylesheet (phase 2 of admin redesign)
|
|
||||||
|
|
||||||
**Track B — Real orders** (setup wizard first, then sequential):
|
| # | Task | Depends on | Est | Status |
|
||||||
1. **Setup wizard & go-live gate** — admin account creation, coming soon page, setup checklist, go-live toggle. See [docs/plans/setup-wizard.md](docs/plans/setup-wizard.md)
|
|---|------|------------|-----|--------|
|
||||||
2. **Wire real product data** — replace PreviewData with Products context on shop pages
|
| | **Done** | | | |
|
||||||
3. **Shipping costs at checkout** — Printify shipping rates or Stripe shipping options
|
| ~~2~~ | ~~`site_live` setting + `Settings.site_live?/0`~~ | — | 30m | done |
|
||||||
|
| ~~3~~ | ~~`Accounts.has_admin?/0` + registration lockdown~~ | — | 1h | done |
|
||||||
|
| ~~8~~ | ~~Coming soon page~~ | 2 | 1h | done |
|
||||||
|
| ~~9~~ | ~~`Setup.setup_status/0` helper~~ | 2, 3 | 30m | done |
|
||||||
|
| ~~10~~ | ~~ThemeHook gate (redirect to coming soon)~~ | 2, 8 | 30m | done |
|
||||||
|
| ~~14~~ | ~~Go live / take offline toggle (on settings page)~~ | 2 | 30m | done |
|
||||||
|
| | **Admin shell chain (priority)** | | | |
|
||||||
|
| 1 | Filesystem restructure (consolidate live/ directories) | — | 2h | |
|
||||||
|
| 6 | Admin shell component (sidebar nav, header) | 1 | 2-3h | |
|
||||||
|
| 7 | Admin root + child layout templates | 1 | 1h | |
|
||||||
|
| 11 | Theme editor back-to-admin link | 6 | 30m | |
|
||||||
|
| 12 | Consolidate settings page | 6, 7 | 2-3h | |
|
||||||
|
| 13 | Admin dashboard (+ setup checklist) | 6, 7, 9 | 2h | |
|
||||||
|
| | **Independent** | | | |
|
||||||
|
| 4 | Admin bar on shop pages | — | 1h | |
|
||||||
|
| 5 | Search (functional search with results) | — | 3-4h | |
|
||||||
|
| | **Needs admin stable** | | | |
|
||||||
|
| 15 | Setup wizard + admin tests | 13 | 1.5h | |
|
||||||
|
| 16 | Variant refinement with live data | — | 2-3h | |
|
||||||
|
| 17 | Wire real product data to shop pages | — | 2-3h | |
|
||||||
|
| 18 | Shipping costs at checkout | 17 | 2-3h | |
|
||||||
|
| | **CSS migration (after admin stable)** | | | |
|
||||||
|
| 19 | Admin design tokens (`admin-tokens.css`) | 12 | 30m | |
|
||||||
|
| 20 | Admin component styles (`app-admin.css`) | 19 | 3-4h | |
|
||||||
|
| 21 | Migrate core_components.ex off DaisyUI | 20 | 2h | |
|
||||||
|
| 22 | Remove DaisyUI | 21 | 1h | |
|
||||||
|
| 23 | CSS migration tests + visual QA | 22 | 1h | |
|
||||||
|
|
||||||
The admin dashboard (A1) and setup wizard (B1) converge: the dashboard IS the setup wizard for new installs.
|
**Total remaining: ~27-33 hours across ~12 sessions**
|
||||||
|
|
||||||
## Usability Issues (from user testing, Feb 2025)
|
## Usability fixes (15/18 done)
|
||||||
|
|
||||||
Issues found during hands-on testing of the deployed prod site on mobile and desktop.
|
Issues from hands-on testing of the deployed prod site (Feb 2025). 15 of 18 complete. The remaining 3 are tracked as features in the task list above (#5 search, #16 variant refinement, #18 shipping costs).
|
||||||
|
|
||||||
**Approach:** One issue at a time, test and verify each fix before moving on.
|
|
||||||
|
|
||||||
**Principles:**
|
|
||||||
- **Semantic, minimal HTML** — achieve everything with the simplest markup possible
|
|
||||||
- **Progressive enhancement** — HTML and CSS first, then LiveView, JS only as a last resort
|
|
||||||
- **Fully accessible** — WCAG 2.1 AA compliant, proper focus management, ARIA where needed, keyboard navigable
|
|
||||||
- **Mobile-first responsive** — design for small screens first, enhance for larger viewports
|
|
||||||
- **Appropriate interactions** — touch-friendly on mobile (swipe, tap), hover/keyboard for desktop users
|
|
||||||
|
|
||||||
### Mobile / touch
|
|
||||||
- [x] Product photos require double-tap on mobile (hover state blocks first tap)
|
|
||||||
- [x] Product photos should be swipeable on mobile (hover-to-reveal is desktop-only)
|
|
||||||
- [x] Product card second image: swipe to reveal on mobile (currently hover-only)
|
|
||||||
|
|
||||||
### Product detail page
|
|
||||||
- [x] PDP image gallery: scroll-snap carousel with dots (mobile), thumbnails + arrows + lightbox (desktop)
|
|
||||||
- [x] Product category breadcrumbs look bad — review styling/layout
|
|
||||||
- [x] Quantity selector on product page doesn't work
|
|
||||||
- [x] Trust badges: two different tick icons, should use sentence case not title case
|
|
||||||
- [ ] Real product variants need testing and refinement with live data
|
|
||||||
|
|
||||||
### Cart
|
|
||||||
- [x] Should be able to change quantity in the cart drawer (currently only on cart page?)
|
|
||||||
- [x] Cart drawer button → "view basket" feels redundant — streamline the flow
|
|
||||||
- [ ] Shipping costs: add Stripe shipping options or query Printify for dynamic rates
|
|
||||||
|
|
||||||
### Navigation & links
|
|
||||||
- [x] "Shop the collection" button/link does nothing
|
|
||||||
- [x] Footer "New arrivals" and "Best sellers" links don't go anywhere
|
|
||||||
- [x] Should be able to tap a category badge on product cards to go to that category
|
|
||||||
- [x] Footer social icons should match the "Find me on" icons from the contact page
|
|
||||||
|
|
||||||
### Collections / all products
|
|
||||||
- [x] Categories on all-products page are too spaced out
|
|
||||||
|
|
||||||
### Content pages
|
|
||||||
- [x] Hero title spacing differs between content pages (about, delivery, etc.) — contact is fine
|
|
||||||
|
|
||||||
### Sale / filtering
|
|
||||||
- [x] Should there be a "Sale" section or filter for discounted products?
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
- [x] 404 page is broken (CSS path was wrong — `/assets/app.css` instead of `/assets/css/app.css`)
|
|
||||||
|
|
||||||
### Search (deferred — after usability fixes)
|
|
||||||
- [ ] Search doesn't work (modal opens but no results/functionality) — see [docs/plans/search.md](docs/plans/search.md)
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,15 @@ defmodule SimpleshopTheme.Accounts do
|
|||||||
"""
|
"""
|
||||||
def get_user!(id), do: Repo.get!(User, id)
|
def get_user!(id), do: Repo.get!(User, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns whether an admin user exists.
|
||||||
|
|
||||||
|
SimpleShop is single-tenant — any user in the database is the admin.
|
||||||
|
"""
|
||||||
|
def has_admin? do
|
||||||
|
Repo.exists?(User)
|
||||||
|
end
|
||||||
|
|
||||||
## User registration
|
## User registration
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|||||||
@ -116,6 +116,22 @@ defmodule SimpleshopTheme.Settings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns whether the shop is live (visible to the public).
|
||||||
|
|
||||||
|
Defaults to `false` for fresh installs.
|
||||||
|
"""
|
||||||
|
def site_live? do
|
||||||
|
get_setting("site_live", false) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sets whether the shop is live (visible to the public).
|
||||||
|
"""
|
||||||
|
def set_site_live(live?) when is_boolean(live?) do
|
||||||
|
put_setting("site_live", live?, "boolean")
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a setting by key.
|
Deletes a setting by key.
|
||||||
"""
|
"""
|
||||||
|
|||||||
33
lib/simpleshop_theme/setup.ex
Normal file
33
lib/simpleshop_theme/setup.ex
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
defmodule SimpleshopTheme.Setup do
|
||||||
|
@moduledoc """
|
||||||
|
Aggregates setup status checks for the admin setup flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias SimpleshopTheme.{Accounts, Products, Settings}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a map describing the current setup status.
|
||||||
|
|
||||||
|
Used by the admin setup checklist and ThemeHook gate to determine
|
||||||
|
what's been completed and whether the shop can go live.
|
||||||
|
"""
|
||||||
|
def setup_status do
|
||||||
|
conn = Products.get_provider_connection_by_type("printify")
|
||||||
|
product_count = Products.count_products_for_connection(conn && conn.id)
|
||||||
|
|
||||||
|
printify_connected = conn != nil and conn.api_key_encrypted != nil
|
||||||
|
products_synced = product_count > 0
|
||||||
|
stripe_connected = Settings.has_secret?("stripe_api_key")
|
||||||
|
site_live = Settings.site_live?()
|
||||||
|
|
||||||
|
%{
|
||||||
|
admin_created: Accounts.has_admin?(),
|
||||||
|
printify_connected: printify_connected,
|
||||||
|
products_synced: products_synced,
|
||||||
|
product_count: product_count,
|
||||||
|
stripe_connected: stripe_connected,
|
||||||
|
site_live: site_live,
|
||||||
|
can_go_live: printify_connected and products_synced and stripe_connected
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -6,7 +6,11 @@ defmodule SimpleshopThemeWeb.AdminLive.Settings do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
{:ok, socket |> assign(:page_title, "Credentials") |> assign_stripe_state()}
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Settings")
|
||||||
|
|> assign(:site_live, Settings.site_live?())
|
||||||
|
|> assign_stripe_state()}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_stripe_state(socket) do
|
defp assign_stripe_state(socket) do
|
||||||
@ -96,6 +100,18 @@ defmodule SimpleshopThemeWeb.AdminLive.Settings do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("toggle_site_live", _params, socket) do
|
||||||
|
new_value = !socket.assigns.site_live
|
||||||
|
{:ok, _} = Settings.set_site_live(new_value)
|
||||||
|
|
||||||
|
message = if new_value, do: "Shop is now live", else: "Shop taken offline"
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:site_live, new_value)
|
||||||
|
|> put_flash(:info, message)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("toggle_advanced", _params, socket) do
|
def handle_event("toggle_advanced", _params, socket) do
|
||||||
{:noreply, assign(socket, :advanced_open, !socket.assigns.advanced_open)}
|
{:noreply, assign(socket, :advanced_open, !socket.assigns.advanced_open)}
|
||||||
end
|
end
|
||||||
@ -106,10 +122,50 @@ defmodule SimpleshopThemeWeb.AdminLive.Settings do
|
|||||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
<div class="max-w-2xl">
|
<div class="max-w-2xl">
|
||||||
<.header>
|
<.header>
|
||||||
Credentials
|
Settings
|
||||||
<:subtitle>Connect payment providers and manage API keys</:subtitle>
|
<:subtitle>Shop status, payment providers, and API keys</:subtitle>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
<section class="mt-10">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h2 class="text-lg font-semibold">Shop status</h2>
|
||||||
|
<%= if @site_live do %>
|
||||||
|
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
|
||||||
|
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
|
||||||
|
Offline
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-zinc-600">
|
||||||
|
<%= if @site_live do %>
|
||||||
|
Your shop is visible to the public.
|
||||||
|
<% else %>
|
||||||
|
Your shop is offline. Visitors see a "coming soon" page.
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button
|
||||||
|
phx-click="toggle_site_live"
|
||||||
|
class={[
|
||||||
|
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
|
||||||
|
if(@site_live,
|
||||||
|
do: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset",
|
||||||
|
else: "bg-green-600 text-white hover:bg-green-500"
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<%= if @site_live do %>
|
||||||
|
<.icon name="hero-eye-slash-mini" class="size-4" /> Take offline
|
||||||
|
<% else %>
|
||||||
|
<.icon name="hero-eye-mini" class="size-4" /> Go live
|
||||||
|
<% end %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-10">
|
<section class="mt-10">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-lg font-semibold">Stripe</h2>
|
<h2 class="text-lg font-semibold">Stripe</h2>
|
||||||
|
|||||||
22
lib/simpleshop_theme_web/live/shop_live/coming_soon.ex
Normal file
22
lib/simpleshop_theme_web/live/shop_live/coming_soon.ex
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
defmodule SimpleshopThemeWeb.ShopLive.ComingSoon do
|
||||||
|
use SimpleshopThemeWeb, :live_view
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, assign(socket, :page_title, "Coming soon")}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<main class="flex min-h-screen items-center justify-center px-6 text-center" role="main">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight sm:text-4xl">{@theme_settings.site_name}</h1>
|
||||||
|
<p class="mt-4 text-lg text-[var(--t-text-muted)]">
|
||||||
|
We're getting things ready. Check back soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -48,9 +48,15 @@ defmodule SimpleshopThemeWeb.UserLive.Registration do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
changeset = Accounts.change_user_email(%User{}, %{}, validate_unique: false)
|
if Accounts.has_admin?() do
|
||||||
|
{:ok,
|
||||||
{:ok, assign_form(socket, changeset), temporary_assigns: [form: nil]}
|
socket
|
||||||
|
|> put_flash(:error, "Registration is closed")
|
||||||
|
|> redirect(to: ~p"/users/log-in")}
|
||||||
|
else
|
||||||
|
changeset = Accounts.change_user_email(%User{}, %{}, validate_unique: false)
|
||||||
|
{:ok, assign_form(socket, changeset), temporary_assigns: [form: nil]}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|||||||
@ -32,10 +32,19 @@ defmodule SimpleshopThemeWeb.Router do
|
|||||||
scope "/", SimpleshopThemeWeb do
|
scope "/", SimpleshopThemeWeb do
|
||||||
pipe_through [:browser, :shop]
|
pipe_through [:browser, :shop]
|
||||||
|
|
||||||
|
live_session :coming_soon,
|
||||||
|
layout: {SimpleshopThemeWeb.Layouts, :shop},
|
||||||
|
on_mount: [
|
||||||
|
{SimpleshopThemeWeb.ThemeHook, :mount_theme}
|
||||||
|
] do
|
||||||
|
live "/coming-soon", ShopLive.ComingSoon, :index
|
||||||
|
end
|
||||||
|
|
||||||
live_session :public_shop,
|
live_session :public_shop,
|
||||||
layout: {SimpleshopThemeWeb.Layouts, :shop},
|
layout: {SimpleshopThemeWeb.Layouts, :shop},
|
||||||
on_mount: [
|
on_mount: [
|
||||||
{SimpleshopThemeWeb.ThemeHook, :mount_theme},
|
{SimpleshopThemeWeb.ThemeHook, :mount_theme},
|
||||||
|
{SimpleshopThemeWeb.ThemeHook, :require_site_live},
|
||||||
{SimpleshopThemeWeb.CartHook, :mount_cart}
|
{SimpleshopThemeWeb.CartHook, :mount_cart}
|
||||||
] do
|
] do
|
||||||
live "/", ShopLive.Home, :index
|
live "/", ShopLive.Home, :index
|
||||||
|
|||||||
@ -4,6 +4,12 @@ defmodule SimpleshopThemeWeb.ThemeHook do
|
|||||||
|
|
||||||
Mounted in the public_shop live_session alongside CartHook.
|
Mounted in the public_shop live_session alongside CartHook.
|
||||||
Eliminates the identical theme-loading boilerplate from every shop LiveView.
|
Eliminates the identical theme-loading boilerplate from every shop LiveView.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
- `:mount_theme` — loads theme settings, CSS, and media assigns
|
||||||
|
- `:require_site_live` — redirects unauthenticated visitors to /coming-soon
|
||||||
|
when the shop is not live
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Phoenix.Component, only: [assign: 3]
|
import Phoenix.Component, only: [assign: 3]
|
||||||
@ -35,4 +41,21 @@ defmodule SimpleshopThemeWeb.ThemeHook do
|
|||||||
|
|
||||||
{:cont, socket}
|
{:cont, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_mount(:require_site_live, _params, session, socket) do
|
||||||
|
cond do
|
||||||
|
Settings.site_live?() ->
|
||||||
|
{:cont, socket}
|
||||||
|
|
||||||
|
session["user_token"] ->
|
||||||
|
{:cont, socket}
|
||||||
|
|
||||||
|
not SimpleshopTheme.Accounts.has_admin?() ->
|
||||||
|
# Fresh install — no admin yet, show the demo shop
|
||||||
|
{:cont, socket}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:halt, Phoenix.LiveView.redirect(socket, to: "/coming-soon")}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,6 +6,22 @@ defmodule SimpleshopTheme.AccountsTest do
|
|||||||
import SimpleshopTheme.AccountsFixtures
|
import SimpleshopTheme.AccountsFixtures
|
||||||
alias SimpleshopTheme.Accounts.{User, UserToken}
|
alias SimpleshopTheme.Accounts.{User, UserToken}
|
||||||
|
|
||||||
|
describe "has_admin?/0" do
|
||||||
|
test "returns false when no users exist" do
|
||||||
|
refute Accounts.has_admin?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true when a user exists" do
|
||||||
|
user_fixture()
|
||||||
|
assert Accounts.has_admin?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true with an unconfirmed user" do
|
||||||
|
unconfirmed_user_fixture()
|
||||||
|
assert Accounts.has_admin?()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "get_user_by_email/1" do
|
describe "get_user_by_email/1" do
|
||||||
test "does not return the user if the email does not exist" do
|
test "does not return the user if the email does not exist" do
|
||||||
refute Accounts.get_user_by_email("unknown@example.com")
|
refute Accounts.get_user_by_email("unknown@example.com")
|
||||||
|
|||||||
@ -192,6 +192,25 @@ defmodule SimpleshopTheme.SettingsTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "site_live?/0 and set_site_live/1" do
|
||||||
|
test "defaults to false when no setting exists" do
|
||||||
|
refute Settings.site_live?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true after setting site live" do
|
||||||
|
assert {:ok, _} = Settings.set_site_live(true)
|
||||||
|
assert Settings.site_live?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns false after setting site offline" do
|
||||||
|
assert {:ok, _} = Settings.set_site_live(true)
|
||||||
|
assert Settings.site_live?()
|
||||||
|
|
||||||
|
assert {:ok, _} = Settings.set_site_live(false)
|
||||||
|
refute Settings.site_live?()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "delete_setting/1" do
|
describe "delete_setting/1" do
|
||||||
test "deletes an existing setting" do
|
test "deletes an existing setting" do
|
||||||
{:ok, _} = Settings.put_setting("to_delete", "value")
|
{:ok, _} = Settings.put_setting("to_delete", "value")
|
||||||
|
|||||||
94
test/simpleshop_theme/setup_test.exs
Normal file
94
test/simpleshop_theme/setup_test.exs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
defmodule SimpleshopTheme.SetupTest do
|
||||||
|
use SimpleshopTheme.DataCase, async: false
|
||||||
|
|
||||||
|
alias SimpleshopTheme.{Setup, Settings, Products}
|
||||||
|
|
||||||
|
import SimpleshopTheme.AccountsFixtures
|
||||||
|
|
||||||
|
describe "setup_status/0" do
|
||||||
|
test "returns all false on fresh install" do
|
||||||
|
status = Setup.setup_status()
|
||||||
|
|
||||||
|
refute status.admin_created
|
||||||
|
refute status.printify_connected
|
||||||
|
refute status.products_synced
|
||||||
|
assert status.product_count == 0
|
||||||
|
refute status.stripe_connected
|
||||||
|
refute status.site_live
|
||||||
|
refute status.can_go_live
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects admin created" do
|
||||||
|
user_fixture()
|
||||||
|
status = Setup.setup_status()
|
||||||
|
|
||||||
|
assert status.admin_created
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects stripe connected" do
|
||||||
|
{:ok, _} = Settings.put_secret("stripe_api_key", "sk_test_abc123")
|
||||||
|
status = Setup.setup_status()
|
||||||
|
|
||||||
|
assert status.stripe_connected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects site live" do
|
||||||
|
{:ok, _} = Settings.set_site_live(true)
|
||||||
|
status = Setup.setup_status()
|
||||||
|
|
||||||
|
assert status.site_live
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects printify connected with products" do
|
||||||
|
{:ok, conn} =
|
||||||
|
Products.create_provider_connection(%{
|
||||||
|
name: "Test",
|
||||||
|
provider_type: "printify",
|
||||||
|
api_key: "test_api_key"
|
||||||
|
})
|
||||||
|
|
||||||
|
status = Setup.setup_status()
|
||||||
|
assert status.printify_connected
|
||||||
|
refute status.products_synced
|
||||||
|
assert status.product_count == 0
|
||||||
|
|
||||||
|
# Add a product
|
||||||
|
{:ok, _product} =
|
||||||
|
Products.create_product(%{
|
||||||
|
title: "Test product",
|
||||||
|
provider_product_id: "ext-1",
|
||||||
|
provider_connection_id: conn.id,
|
||||||
|
status: "active"
|
||||||
|
})
|
||||||
|
|
||||||
|
status = Setup.setup_status()
|
||||||
|
assert status.products_synced
|
||||||
|
assert status.product_count == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can_go_live requires printify, products, and stripe" 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"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Still missing stripe
|
||||||
|
refute Setup.setup_status().can_go_live
|
||||||
|
|
||||||
|
# Add stripe
|
||||||
|
{:ok, _} = Settings.put_secret("stripe_api_key", "sk_test_abc123")
|
||||||
|
|
||||||
|
assert Setup.setup_status().can_go_live
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -11,6 +11,44 @@ defmodule SimpleshopThemeWeb.AdminLive.SettingsTest do
|
|||||||
%{user: user}
|
%{user: user}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "shop status toggle" do
|
||||||
|
setup %{conn: conn, user: user} do
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows offline status by default", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
assert html =~ "Offline"
|
||||||
|
assert html =~ "coming soon"
|
||||||
|
assert html =~ "Go live"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can go live", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
html = render_click(view, "toggle_site_live")
|
||||||
|
|
||||||
|
assert html =~ "Shop is now live"
|
||||||
|
assert html =~ "Live"
|
||||||
|
assert html =~ "Take offline"
|
||||||
|
assert Settings.site_live?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can take offline after going live", %{conn: conn} do
|
||||||
|
{:ok, _} = Settings.set_site_live(true)
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
|
html = render_click(view, "toggle_site_live")
|
||||||
|
|
||||||
|
assert html =~ "Shop taken offline"
|
||||||
|
assert html =~ "Offline"
|
||||||
|
assert html =~ "Go live"
|
||||||
|
refute Settings.site_live?()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "unauthenticated" do
|
describe "unauthenticated" do
|
||||||
test "redirects to login", %{conn: conn} do
|
test "redirects to login", %{conn: conn} do
|
||||||
{:error, redirect} = live(conn, ~p"/admin/settings")
|
{:error, redirect} = live(conn, ~p"/admin/settings")
|
||||||
@ -28,7 +66,7 @@ defmodule SimpleshopThemeWeb.AdminLive.SettingsTest do
|
|||||||
test "renders setup form when Stripe is not configured", %{conn: conn} do
|
test "renders setup form when Stripe is not configured", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/admin/settings")
|
{:ok, _view, html} = live(conn, ~p"/admin/settings")
|
||||||
|
|
||||||
assert html =~ "Credentials"
|
assert html =~ "Settings"
|
||||||
assert html =~ "Not connected"
|
assert html =~ "Not connected"
|
||||||
assert html =~ "Connect Stripe"
|
assert html =~ "Connect Stripe"
|
||||||
assert html =~ "Stripe dashboard"
|
assert html =~ "Stripe dashboard"
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
defmodule SimpleshopThemeWeb.ShopLive.ComingSoonTest do
|
||||||
|
use SimpleshopThemeWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import SimpleshopTheme.AccountsFixtures
|
||||||
|
|
||||||
|
alias SimpleshopTheme.Settings
|
||||||
|
|
||||||
|
describe "coming soon page" do
|
||||||
|
test "renders when site is not live and admin exists", %{conn: conn} do
|
||||||
|
user_fixture()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/coming-soon")
|
||||||
|
|
||||||
|
assert html =~ "Coming soon"
|
||||||
|
assert html =~ "getting things ready"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays the shop name", %{conn: conn} do
|
||||||
|
{:ok, _} = Settings.update_theme_settings(%{site_name: "My Test Shop"})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/coming-soon")
|
||||||
|
|
||||||
|
assert html =~ "My Test Shop"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "site live gate" do
|
||||||
|
test "redirects unauthenticated visitors to coming soon when not live", %{conn: conn} do
|
||||||
|
# Create admin so the gate activates (fresh installs bypass)
|
||||||
|
user_fixture()
|
||||||
|
|
||||||
|
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows authenticated admin through when not live", %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Shop the collection"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows everyone through when site is live", %{conn: conn} do
|
||||||
|
user_fixture()
|
||||||
|
{:ok, _} = Settings.set_site_live(true)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Shop the collection"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows everyone through on fresh install (no admin)", %{conn: conn} do
|
||||||
|
# No admin created — fresh install shows the demo shop
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Shop the collection"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "gates all public shop routes", %{conn: conn} do
|
||||||
|
user_fixture()
|
||||||
|
|
||||||
|
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/about")
|
||||||
|
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/collections/all")
|
||||||
|
assert {:error, {:redirect, %{to: "/coming-soon"}}} = live(conn, ~p"/cart")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -23,8 +23,8 @@ defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "CSS selector consistency" do
|
describe "CSS selector consistency" do
|
||||||
test "shop home page has .themed with data attributes", %{conn: conn} do
|
test "shop home page has .themed with data attributes", %{conn: conn, user: user} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/")
|
{:ok, _view, html} = live(log_in_user(conn, user), ~p"/")
|
||||||
|
|
||||||
# Verify themed element exists with theme data attributes
|
# Verify themed element exists with theme data attributes
|
||||||
assert html =~ ~r/<div[^>]*class="themed/
|
assert html =~ ~r/<div[^>]*class="themed/
|
||||||
@ -50,11 +50,11 @@ defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
|
|||||||
# Set a specific theme configuration
|
# Set a specific theme configuration
|
||||||
{:ok, _settings} = Settings.apply_preset(:night)
|
{:ok, _settings} = Settings.apply_preset(:night)
|
||||||
|
|
||||||
# Check shop page
|
# Check shop page (logged in since site_live is false by default)
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
{:ok, _view, shop_html} = live(conn, ~p"/")
|
{:ok, _view, shop_html} = live(conn, ~p"/")
|
||||||
|
|
||||||
# Check preview (authenticated)
|
# Check preview (already authenticated)
|
||||||
conn = log_in_user(conn, user)
|
|
||||||
{:ok, _view, preview_html} = live(conn, ~p"/admin/theme")
|
{:ok, _view, preview_html} = live(conn, ~p"/admin/theme")
|
||||||
|
|
||||||
# Extract data-mood values from both
|
# Extract data-mood values from both
|
||||||
@ -66,7 +66,9 @@ defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
|
|||||||
assert shop_mood == "dark"
|
assert shop_mood == "dark"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "theme settings changes are reflected on shop page", %{conn: conn} do
|
test "theme settings changes are reflected on shop page", %{conn: conn, user: user} do
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
|
||||||
# Start with minimal preset (neutral mood)
|
# Start with minimal preset (neutral mood)
|
||||||
{:ok, _settings} = Settings.apply_preset(:minimal)
|
{:ok, _settings} = Settings.apply_preset(:minimal)
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,21 @@ defmodule SimpleshopThemeWeb.UserLive.RegistrationTest do
|
|||||||
import SimpleshopTheme.AccountsFixtures
|
import SimpleshopTheme.AccountsFixtures
|
||||||
|
|
||||||
describe "Registration page" do
|
describe "Registration page" do
|
||||||
test "renders registration page", %{conn: conn} do
|
test "renders registration page when no admin exists", %{conn: conn} do
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/register")
|
{:ok, _lv, html} = live(conn, ~p"/users/register")
|
||||||
|
|
||||||
assert html =~ "Register"
|
assert html =~ "Register"
|
||||||
assert html =~ "Log in"
|
assert html =~ "Log in"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "redirects to login when admin already exists", %{conn: conn} do
|
||||||
|
user_fixture()
|
||||||
|
|
||||||
|
assert {:error,
|
||||||
|
{:redirect, %{to: "/users/log-in", flash: %{"error" => "Registration is closed"}}}} =
|
||||||
|
live(conn, ~p"/users/register")
|
||||||
|
end
|
||||||
|
|
||||||
test "redirects if already logged in", %{conn: conn} do
|
test "redirects if already logged in", %{conn: conn} do
|
||||||
result =
|
result =
|
||||||
conn
|
conn
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user