diff --git a/PROGRESS.md b/PROGRESS.md index 4de8c3f..97d4707 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -20,7 +20,15 @@ - Transactional emails (order confirmation, shipping notification) - 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). Next up: usability fixes from user testing, then remaining Tier 2 items (Litestream backup, e2e tests). +**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. + +## Next Up: Setup Wizard → Real Products → Shipping + +Three linked tasks to make the shop fully functional for real orders: + +1. **Setup wizard & go-live gate** — coming soon page for public visitors, admin 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 (depends on setup wizard being done so there's a gate while testing) +3. **Shipping costs at checkout** — Printify shipping rates or Stripe shipping options (depends on real products being connected) ## Usability Issues (from user testing, Feb 2025) diff --git a/docs/plans/setup-wizard.md b/docs/plans/setup-wizard.md new file mode 100644 index 0000000..7be776e --- /dev/null +++ b/docs/plans/setup-wizard.md @@ -0,0 +1,274 @@ +# Plan: Setup wizard and "go live" gate + +Status: Pending + +## Overview + +The shop currently shows demo/preview data to all visitors with no admin account creation flow, open registration, and no gate between setup and going live. This plan adds a proper first-run setup wizard that: + +1. Creates the single admin account (replacing the open `/users/register` endpoint) +2. Guides the admin through connecting Printify and Stripe +3. Gates the public shop behind a "coming soon" page until setup is complete +4. Provides a "go live" toggle + +## Dependency chain + +``` +Setup wizard/gate → Real product data on shop pages → Shipping costs at checkout +``` + +This plan covers the first piece. The other two are separate tasks that build on top. + +## Current state + +- `/users/register` is open to anyone — no user limit, no role system +- All registered users get full admin access to `/admin/*` routes +- No "first run" detection — fresh installs show the demo shop to everyone +- No way to close registration after the first user + +## Design: single-admin, closed registration + +SimpleShop is single-tenant: one shop, one admin. The setup wizard replaces the generic registration flow entirely. + +### Fresh install flow + +``` +1. Fresh install (no users in DB) + → All public routes redirect to /setup + → /setup shows "Create your admin account" form (email input) + → Magic link email sent → user confirms → account created + → Registration permanently closed (one user = done) + +2. Admin logged in, setup incomplete + → Admin sees /admin/setup checklist + → Connect Printify → Sync products → Connect Stripe + → Public visitors see "coming soon" page + +3. Admin clicks "Go live" + → Public visitors see the real shop + → Admin can take offline again if needed +``` + +### Registration lockdown + +- `Accounts.has_admin?/0` — checks if any user exists (simple count > 0) +- When `has_admin?()` is true, `/users/register` redirects to `/users/log-in` +- The `/setup` route also redirects to `/users/log-in` if admin already exists +- No new users can ever be created after the first one + +## Prerequisites check + +The setup wizard checks three things before allowing "go live": + +| Step | How to check | Module/function | +|------|-------------|-----------------| +| Printify connected | `Products.get_provider_connection_by_type("printify")` returns a connection with a non-nil `api_key_encrypted` | `SimpleshopTheme.Products` | +| Products synced | `Products.count_products_for_connection(conn.id) > 0` | `SimpleshopTheme.Products` | +| Stripe connected | `Settings.has_secret?("stripe_api_key")` | `SimpleshopTheme.Settings` | + +Optional (nice-to-have, not blocking go-live): +- Stripe webhook configured: `Settings.has_secret?("stripe_webhook_signing_secret")` +- Shop name customised: `theme_settings.site_name != "SimpleShop"` (or similar default check) + +## Changes + +### 1. `Accounts.has_admin?/0` and registration lockdown + +**File:** `lib/simpleshop_theme/accounts.ex` + +- Add `has_admin?/0` — `Repo.exists?(User)` (any user = admin exists) +- This is the single check that gates registration + +**File:** `lib/simpleshop_theme_web/live/user_live/registration.ex` + +- In `mount/3`, check `Accounts.has_admin?()` — if true, redirect to `/users/log-in` with a flash like "Registration is closed" + +**File:** `lib/simpleshop_theme_web/router.ex` + +- No route changes needed yet — the LiveView mount handles the redirect + +**Estimate:** 20 mins + +### 2. Add `site_live` setting and setup status + +**File:** `lib/simpleshop_theme/settings.ex` + +- Add `site_live?/0` — reads `get_setting("shop", "site_live")`, returns boolean (default `false`) +- Add `set_site_live/1` — writes `put_setting("shop", "site_live", value)` + +No migration needed — settings table already stores arbitrary key/value pairs. + +**File:** `lib/simpleshop_theme/setup.ex` (new module) + +- `setup_status/0` returns a map: + ```elixir + %{ + admin_created: boolean, + printify_connected: boolean, + products_synced: boolean, + product_count: integer, + stripe_connected: boolean, + site_live: boolean, + can_go_live: boolean # all three service prerequisites met + } + ``` +- Keep it simple — a single function that queries current state, no caching + +**Estimate:** 30 mins + +### 3. Fresh install redirect (no admin exists) + +**File:** `lib/simpleshop_theme_web/hooks/theme_hook.ex` + +ThemeHook already runs on every public shop page mount. Add early check: + +- If `Accounts.has_admin?()` is false → redirect to `/setup` +- This catches the fresh install case before any other logic runs + +**File:** `lib/simpleshop_theme_web/live/setup_live.ex` (new) + +A simple public LiveView at `/setup` that: +- If admin already exists → redirect to `/users/log-in` +- If no admin → show "Welcome to SimpleShop" with email input form +- On submit → calls `Accounts.register_user/1` and sends magic link +- Shows "Check your email" confirmation + +This reuses the existing registration logic but with a different UI (setup-focused, not generic registration). + +**File:** `lib/simpleshop_theme_web/router.ex` + +- Add `/setup` route in a minimal live_session (no ThemeHook, no CartHook — avoids the redirect loop) + +**Estimate:** 1.5 hours + +### 4. "Coming soon" page for public visitors + +**File:** `lib/simpleshop_theme_web/hooks/theme_hook.ex` + +Extend the ThemeHook logic (after the fresh install check): + +- If `site_live?()` is false AND user is not authenticated → redirect to `/coming-soon` + +**File:** `lib/simpleshop_theme_web/live/shop_live/coming_soon.ex` (new) + +Minimal LiveView: +- Uses the shop root layout (gets theme styling) but no nav/footer +- Shows site name/logo, "Coming soon" heading, optional tagline +- No redirect loop — this page itself doesn't trigger the gate + +**File:** `lib/simpleshop_theme_web/router.ex` + +- Add `/coming-soon` route in the public shop live_session but mark it as exempt from the gate (via assign or separate handling in ThemeHook) + +**Estimate:** 1 hour + +### 5. Admin setup checklist page + +**File:** `lib/simpleshop_theme_web/live/admin/setup_live.ex` (new) + +Admin page at `/admin/setup` showing: + +- **Step 1: Connect Printify** — status indicator (done/not done), link to `/admin/providers` +- **Step 2: Sync products** — status (X products synced / none yet), link to `/admin/providers` +- **Step 3: Connect Stripe** — status indicator, link to `/admin/settings` +- **Go live button** — enabled only when `can_go_live` is true +- **Take offline button** — when already live, allows switching back + +Each step shows what to do and links to where to do it. Feels like guided onboarding, not a settings dump. + +**Files:** +- `lib/simpleshop_theme_web/live/admin/setup_live.ex` +- Router update to add the route +- Admin nav update to include "Setup" link (prominent when not live) + +**Estimate:** 2 hours + +### 6. Admin bar "not live" indicator + +**File:** `lib/simpleshop_theme_web/components/shop_components/layout.ex` + +- When shop is not live, show a banner in the admin bar: "Your shop is not live — [Complete setup →]" +- When shop is live, the setup page becomes a less prominent settings link + +**Estimate:** 30 mins + +### 7. Post-login redirect for fresh admin + +**File:** `lib/simpleshop_theme_web/user_auth.ex` + +- After confirming magic link (first login ever), redirect to `/admin/setup` instead of `/` +- Subsequent logins go to `/` as normal (or `/admin/setup` if not live yet) + +Could be as simple as: if `site_live?()` is false, `signed_in_path` returns `/admin/setup` + +**Estimate:** 20 mins + +## Task breakdown + +| # | Task | Files | Estimate | +|---|------|-------|----------| +| 1 | `has_admin?/0` + lock registration | accounts.ex, registration.ex | 20 mins | +| 2 | `site_live?/0`, `set_site_live/1`, `Setup.setup_status/0` | settings.ex, setup.ex | 30 mins | +| 3 | Fresh install setup page (`/setup`) | setup_live.ex, router.ex | 1.5 hours | +| 4 | Coming soon page + ThemeHook gate | coming_soon.ex, theme_hook.ex, router.ex | 1 hour | +| 5 | Admin setup checklist (`/admin/setup`) | admin/setup_live.ex, router.ex | 2 hours | +| 6 | Admin bar "not live" indicator | layout.ex | 30 mins | +| 7 | Post-login redirect to setup | user_auth.ex | 20 mins | +| 8 | Tests | test files | 1.5 hours | + +**Total estimate:** ~7-8 hours across 3-4 sessions + +## Downstream tasks (separate plans) + +### Wire real product data into shop pages + +Currently `ShopLive.Home` and other pages use `PreviewData.products()` / `PreviewData.categories()`. Once real products exist: + +- Replace `PreviewData` calls with `Products.list_products/1` queries +- Fall back to preview data when no real products exist (keeps the theme editor working) +- Collection page already uses real data via the Products context — verify it works end-to-end + +**Estimate:** 1-2 hours + +### Shipping costs at checkout + +Options: +- **Stripe shipping rates:** Define fixed shipping rates in Stripe, pass them to Checkout Session +- **Printify shipping query:** Call Printify's shipping API to get real rates based on destination +- **Hybrid:** Use Printify rates but configure via Stripe's dynamic shipping option + +**Estimate:** 2-4 hours (needs research) + +## Testing + +- `has_admin?/0` returns false with no users, true with one +- Registration redirects when admin exists +- Fresh install: all public routes redirect to `/setup` +- `/setup` creates admin account and sends magic link +- `/setup` redirects to login when admin already exists +- Coming soon page renders when not live + not authenticated +- Admin bypasses coming soon and sees the full shop +- Setup checklist shows correct status for each step +- Go live button works and flips the gate +- After going live, public visitors see the real shop +- Take offline button works + +## Platform compatibility (Tier 5) + +This design is forward-compatible with the hosted platform (per-tenant databases): + +- **`has_admin?/0`** checks the local DB — naturally scoped to the tenant +- **`site_live?/0`** is per-DB — each tenant goes live independently +- **`setup_status/0`** queries local state — all per-tenant + +The only difference is how the admin account arrives: +- **Self-hosted:** Admin visits `/setup`, enters email, confirms magic link +- **Platform:** Provisioning layer pre-seeds the admin user in the tenant DB during sign-up. `has_admin?()` is already true, so `/setup` redirects to login and the setup wizard starts at service connections + +No code changes needed for this — the platform layer just calls `Accounts.register_user/1` directly instead of going through the `/setup` UI. The setup checklist, go-live gate, and coming soon page all work identically in both modes. + +## Open questions + +- Should the coming soon page be themeable (use current theme colours/fonts) or a fixed design? Leaning themeable — it's a nice preview of the brand. +- Should `/admin/setup` replace `/admin/settings` or sit alongside it? Alongside, with setup as the landing page post-registration. +- Should we cache `has_admin?/0` to avoid a DB query on every page load? Probably fine uncached for single-tenant — one tiny query. Could use an ETS flag if it becomes an issue.