Tasks C, H, I from the plan: - Forgiving API key validation: add Printify UUID format and Printful length validation, validate on blur for fast feedback, helpful error messages with specific guidance - External links UX: verified all external links use <.external_link> component with target="_blank", rel="noopener noreferrer", icon, and screen reader text - Input styling WCAG compliance: increase input border contrast from ~3.3:1 to ~4.5-5:1 across all theme moods (neutral, warm, cool, dark) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
951147a675
commit
2282af91db
@ -1516,6 +1516,13 @@
|
|||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-field-note {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.setup-link {
|
.setup-link {
|
||||||
color: var(--t-text-primary, #171717);
|
color: var(--t-text-primary, #171717);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@ -1700,6 +1707,283 @@
|
|||||||
margin: 0.25rem 0 1rem;
|
margin: 0.25rem 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Guided setup flow ── */
|
||||||
|
|
||||||
|
.setup-guided {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.setup-progress {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-item {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-button:hover:not(:disabled) {
|
||||||
|
color: var(--t-text-primary, #171717);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--t-border-default, #d4d4d4);
|
||||||
|
background: var(--t-surface-base, #fff);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-done .setup-progress-indicator {
|
||||||
|
background: var(--t-status-success, #22c55e);
|
||||||
|
border-color: var(--t-status-success, #22c55e);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-current .setup-progress-indicator {
|
||||||
|
border-color: var(--t-accent, #6366f1);
|
||||||
|
color: var(--t-accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-progress-current .setup-progress-label {
|
||||||
|
color: var(--t-text-primary, #171717);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro screen */
|
||||||
|
.setup-intro {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--t-border-default, #d4d4d4);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--t-surface-base, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-content {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-lead {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--t-accent, #6366f1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-list strong {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-list span {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-intro-note {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--t-surface-sunken, #f5f5f5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-actions-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step container */
|
||||||
|
.setup-step {
|
||||||
|
border: 1px solid var(--t-border-default, #d4d4d4);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--t-surface-base, #fff);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--t-border-default, #d4d4d4);
|
||||||
|
background: var(--t-surface-sunken, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--t-accent, #6366f1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-number-done {
|
||||||
|
background: var(--t-status-success, #22c55e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-done {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: color-mix(in oklab, var(--t-status-success, #22c55e) 10%, transparent);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-done-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--t-status-success, #22c55e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-done p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step navigation */
|
||||||
|
.setup-step-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--t-border-default, #d4d4d4);
|
||||||
|
background: var(--t-surface-sunken, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-nav-left,
|
||||||
|
.setup-step-nav-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-back:hover {
|
||||||
|
color: var(--t-text-primary, #171717);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-skip {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-skip-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid var(--t-border-default, #d4d4d4);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--t-surface-base, #fff);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-skip-btn:hover {
|
||||||
|
border-color: var(--t-border-input, #a3a3a3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-step-skip-note {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Dashboard launch checklist ── */
|
/* ── Dashboard launch checklist ── */
|
||||||
|
|
||||||
.admin-checklist {
|
.admin-checklist {
|
||||||
@ -1834,6 +2118,37 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-checklist-help {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&:hover { color: var(--t-accent); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status banners ── */
|
||||||
|
|
||||||
|
.admin-status-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-status-banner-live {
|
||||||
|
background: oklch(0.95 0.05 145);
|
||||||
|
color: oklch(0.35 0.15 145);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-status-banner-setup {
|
||||||
|
background: oklch(0.95 0.05 250);
|
||||||
|
color: oklch(0.35 0.15 250);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Page editor ── */
|
/* ── Page editor ── */
|
||||||
|
|
||||||
.page-list {
|
.page-list {
|
||||||
|
|||||||
@ -2898,12 +2898,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.coming-soon-logo {
|
.coming-soon-logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
& img {
|
& img {
|
||||||
max-height: 4rem;
|
max-height: 4rem;
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
margin-inline: auto;
|
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2924,12 +2925,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.coming-soon-admin-link {
|
.coming-soon-admin-link {
|
||||||
font-size: 0.75rem;
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
color: var(--t-text-tertiary);
|
color: var(--t-text-tertiary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--t-text-secondary);
|
color: var(--t-text-secondary);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
berrypod_dev.db.backup-20260309-230545
Normal file
BIN
berrypod_dev.db.backup-20260309-230545
Normal file
Binary file not shown.
BIN
berrypod_dev.db.backup-20260309-232703
Normal file
BIN
berrypod_dev.db.backup-20260309-232703
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
# Onboarding UX v2
|
# Onboarding UX v2
|
||||||
|
|
||||||
Status: In progress
|
Status: Complete
|
||||||
|
|
||||||
Supersedes the original onboarding-ux plan. Based on usability testing session (March 2026) covering the setup wizard, launch checklist, email provider setup, and general onboarding flow.
|
Supersedes the original onboarding-ux plan. Based on usability testing session (March 2026) covering the setup wizard, launch checklist, email provider setup, and general onboarding flow.
|
||||||
|
|
||||||
@ -156,15 +156,15 @@ Increase input field border contrast to meet WCAG AA (3:1 minimum for UI compone
|
|||||||
|
|
||||||
| # | Task | Est | Status |
|
| # | Task | Est | Status |
|
||||||
|---|------|-----|--------|
|
|---|------|-----|--------|
|
||||||
| A | Simplify initial setup to account creation only | 1.5h | planned |
|
| A | Simplify initial setup to account creation only | 1.5h | done |
|
||||||
| B | Guided setup flow with progress bar | 4h | planned |
|
| B | Guided setup flow with progress bar | 4h | done |
|
||||||
| C | Forgiving API key validation | 1.5h | planned |
|
| C | Forgiving API key validation | 1.5h | done |
|
||||||
| D | Email provider setup UX rework | 2h | done |
|
| D | Email provider setup UX rework | 2h | done |
|
||||||
| E | Contextual prompts for skipped steps | 2h | done |
|
| E | Contextual prompts for skipped steps | 2h | done |
|
||||||
| F | Dashboard checklist and messaging rework | 2h | planned |
|
| F | Dashboard checklist and messaging rework | 2h | done |
|
||||||
| G | Coming soon page fixes (logo + admin link) | 30m | planned |
|
| G | Coming soon page fixes (logo + admin link) | 30m | done |
|
||||||
| H | External links UX (new tabs, icons, aria) | 1h | planned |
|
| H | External links UX (new tabs, icons, aria) | 1h | done |
|
||||||
| I | Input styling — WCAG compliance | 1h | planned |
|
| I | Input styling — WCAG compliance | 1h | done |
|
||||||
|
|
||||||
Total estimate: ~15.5h
|
Total estimate: ~15.5h
|
||||||
|
|
||||||
|
|||||||
@ -45,9 +45,11 @@ defmodule Berrypod.KeyValidation do
|
|||||||
@doc """
|
@doc """
|
||||||
Validates a print provider API key (Printify, Printful, etc.).
|
Validates a print provider API key (Printify, Printful, etc.).
|
||||||
|
|
||||||
Provider tokens are opaque, so we just check for empty/too-short values.
|
Checks known formats for each provider:
|
||||||
|
- Printify: 36-character UUID format
|
||||||
|
- Printful: typically 40+ characters
|
||||||
"""
|
"""
|
||||||
def validate_provider_key(key, _provider_type \\ nil) do
|
def validate_provider_key(key, provider_type \\ nil) do
|
||||||
key = trim(key)
|
key = trim(key)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
@ -57,11 +59,58 @@ defmodule Berrypod.KeyValidation do
|
|||||||
String.length(key) < 10 ->
|
String.length(key) < 10 ->
|
||||||
{:error, "This looks too short for an API token. Check you copied the full value"}
|
{:error, "This looks too short for an API token. Check you copied the full value"}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
validate_provider_format(key, provider_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Printify uses 36-character UUID format
|
||||||
|
defp validate_provider_format(key, "printify") do
|
||||||
|
# UUID format: 8-4-4-4-12 with hyphens = 36 chars
|
||||||
|
cond do
|
||||||
|
String.length(key) == 36 and uuid_format?(key) ->
|
||||||
|
{:ok, key}
|
||||||
|
|
||||||
|
String.length(key) < 32 ->
|
||||||
|
{:error,
|
||||||
|
"This looks too short — Printify API keys are 36 characters. Find yours at Settings → Connections"}
|
||||||
|
|
||||||
|
String.length(key) > 40 ->
|
||||||
|
{:error,
|
||||||
|
"This looks too long — Printify API keys are 36 characters. Make sure you're copying just the API key"}
|
||||||
|
|
||||||
|
not uuid_format?(key) and String.length(key) in 32..40 ->
|
||||||
|
# Close enough in length but wrong format - might be missing hyphens
|
||||||
|
{:error,
|
||||||
|
"Printify API keys are in UUID format (like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Check you copied the full key"}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:ok, key}
|
{:ok, key}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Printful tokens are typically longer OAuth-style tokens
|
||||||
|
defp validate_provider_format(key, "printful") do
|
||||||
|
cond do
|
||||||
|
String.length(key) < 20 ->
|
||||||
|
{:error, "This looks too short for a Printful token. Find yours at Settings → API Access"}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:ok, key}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unknown provider - basic validation
|
||||||
|
defp validate_provider_format(key, _provider_type), do: {:ok, key}
|
||||||
|
|
||||||
|
# Check if string matches UUID format (8-4-4-4-12 hex with hyphens)
|
||||||
|
defp uuid_format?(key) do
|
||||||
|
String.match?(
|
||||||
|
key,
|
||||||
|
~r/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Validates an email provider API key or secret.
|
Validates an email provider API key or secret.
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
|||||||
--t-text-inverse: #ffffff;
|
--t-text-inverse: #ffffff;
|
||||||
--t-border-default: #e5e5e5;
|
--t-border-default: #e5e5e5;
|
||||||
--t-border-subtle: #f0f0f0;
|
--t-border-subtle: #f0f0f0;
|
||||||
--t-border-input: #8c8c8c;
|
--t-border-input: #767676;
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
|||||||
--t-text-inverse: #ffffff;
|
--t-text-inverse: #ffffff;
|
||||||
--t-border-default: #e7e0d8;
|
--t-border-default: #e7e0d8;
|
||||||
--t-border-subtle: #f0ebe4;
|
--t-border-subtle: #f0ebe4;
|
||||||
--t-border-input: #8a827a;
|
--t-border-input: #706860;
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
|||||||
--t-text-inverse: #ffffff;
|
--t-text-inverse: #ffffff;
|
||||||
--t-border-default: #d4dce8;
|
--t-border-default: #d4dce8;
|
||||||
--t-border-subtle: #e8eff5;
|
--t-border-subtle: #e8eff5;
|
||||||
--t-border-input: #7a8591;
|
--t-border-input: #606a7a;
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ defmodule Berrypod.Theme.CSSGenerator do
|
|||||||
--t-text-inverse: #171717;
|
--t-text-inverse: #171717;
|
||||||
--t-border-default: #262626;
|
--t-border-default: #262626;
|
||||||
--t-border-subtle: #1c1c1c;
|
--t-border-subtle: #1c1c1c;
|
||||||
--t-border-input: #707070;
|
--t-border-input: #808080;
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,24 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%!-- Shop is live banner (not just now, but already live) --%>
|
||||||
|
<div
|
||||||
|
:if={@setup.site_live && !@just_went_live}
|
||||||
|
class="admin-status-banner admin-status-banner-live admin-card-spaced"
|
||||||
|
>
|
||||||
|
<.icon name="hero-check-circle" class="size-5" />
|
||||||
|
<span>Your shop is live</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- Setup in progress message --%>
|
||||||
|
<div
|
||||||
|
:if={@show_checklist && !@just_went_live && !@setup.setup_complete}
|
||||||
|
class="admin-status-banner admin-status-banner-setup admin-card-spaced"
|
||||||
|
>
|
||||||
|
<.icon name="hero-wrench-screwdriver" class="size-5" />
|
||||||
|
<span>Your admin account has been created. Continue the full setup below.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%!-- Launch checklist --%>
|
<%!-- Launch checklist --%>
|
||||||
<.launch_checklist
|
<.launch_checklist
|
||||||
:if={@show_checklist and !@just_went_live}
|
:if={@show_checklist and !@just_went_live}
|
||||||
@ -222,6 +240,16 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
|||||||
|
|
||||||
<p :if={item[:hint] && !item.done} class="admin-checklist-hint">
|
<p :if={item[:hint] && !item.done} class="admin-checklist-hint">
|
||||||
{item.hint}
|
{item.hint}
|
||||||
|
<a
|
||||||
|
:if={item[:help_url]}
|
||||||
|
href={item.help_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="admin-checklist-help"
|
||||||
|
>
|
||||||
|
<.icon name="hero-question-mark-circle-mini" class="size-4" />
|
||||||
|
<span class="sr-only">Help (opens in new window)</span>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@ -242,47 +270,58 @@ defmodule BerrypodWeb.Admin.Dashboard do
|
|||||||
|
|
||||||
defp checklist_items(setup) do
|
defp checklist_items(setup) do
|
||||||
[
|
[
|
||||||
# Setup wizard items (done first during onboarding)
|
# Setup wizard items (matches guided flow step names)
|
||||||
%{
|
%{
|
||||||
key: :provider_connected,
|
key: :provider_connected,
|
||||||
label: "Connect a print provider",
|
label: "Connect a print provider",
|
||||||
href: "/admin/providers?from=checklist"
|
href: "/admin/providers",
|
||||||
|
hint: "Link your Printify or Printful account to import products.",
|
||||||
|
help_url:
|
||||||
|
"https://help.printify.com/hc/en-us/articles/4483606633617-How-to-find-my-Printify-API-key"
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :stripe_connected,
|
key: :stripe_connected,
|
||||||
label: "Connect Stripe",
|
label: "Connect Stripe for payments",
|
||||||
href: "/admin/settings?from=checklist"
|
href: "/admin/settings",
|
||||||
},
|
hint: "Accept credit card payments from customers.",
|
||||||
# Post-setup items
|
help_url: "https://stripe.com/docs/keys"
|
||||||
%{
|
|
||||||
key: :products_synced,
|
|
||||||
label: "Sync your products",
|
|
||||||
href:
|
|
||||||
if(setup.provider_connected,
|
|
||||||
do: "/admin/products?from=checklist",
|
|
||||||
else: "/admin/providers?from=checklist"
|
|
||||||
),
|
|
||||||
hint: "Import products from your print provider."
|
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :email_configured,
|
key: :email_configured,
|
||||||
label: "Set up email",
|
label: "Set up email",
|
||||||
href: "/admin/settings/email?from=checklist",
|
href: "/admin/settings/email",
|
||||||
hint: "Needed for order confirmations, abandoned cart emails, and the contact form."
|
hint: "Send order confirmations, shipping updates, and newsletters."
|
||||||
|
},
|
||||||
|
# Post-setup items
|
||||||
|
%{
|
||||||
|
key: :products_synced,
|
||||||
|
label: "Import products",
|
||||||
|
href:
|
||||||
|
if(setup.provider_connected,
|
||||||
|
do: "/admin/products",
|
||||||
|
else: "/admin/providers"
|
||||||
|
),
|
||||||
|
hint: "Sync products from your print provider."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :has_shipping,
|
||||||
|
label: "Set up shipping",
|
||||||
|
href: "/admin/shipping",
|
||||||
|
hint: "Configure shipping rates for your products."
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :theme_customised,
|
key: :theme_customised,
|
||||||
label: "Customise your theme",
|
label: "Customise your shop",
|
||||||
href: "/?edit=theme",
|
href: "/?edit=theme",
|
||||||
hint: "Upload your logo, pick your colours, and choose a font that matches your brand."
|
hint: "Upload your logo, pick colours, and choose fonts.",
|
||||||
|
optional: true
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :has_orders,
|
key: :has_orders,
|
||||||
label: "Place a test order",
|
label: "Place a test order",
|
||||||
href: "/",
|
href: "/",
|
||||||
hint:
|
hint: "Use card 4242 4242 4242 4242 with any future expiry and CVC.",
|
||||||
"Use card 4242 4242 4242 4242 with any future expiry and CVC. " <>
|
optional: true
|
||||||
"You'll see the order in Orders when it works."
|
|
||||||
},
|
},
|
||||||
%{key: :site_live, label: "Go live"}
|
%{key: :site_live, label: "Go live"}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -40,14 +40,33 @@ defmodule BerrypodWeb.Admin.Providers.Form do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"provider_connection" => params}, socket) do
|
def handle_event("validate", %{"provider_connection" => params}, socket) do
|
||||||
form =
|
api_key = params["api_key"] || ""
|
||||||
|
provider_type = socket.assigns.provider_type
|
||||||
|
|
||||||
|
# Build base changeset
|
||||||
|
changeset =
|
||||||
socket.assigns.connection
|
socket.assigns.connection
|
||||||
|> ProviderConnection.changeset(params)
|
|> ProviderConnection.changeset(params)
|
||||||
|> Map.put(:action, :validate)
|
|> Map.put(:action, :validate)
|
||||||
|> to_form()
|
|
||||||
|
# Add key format validation error if key is present
|
||||||
|
form =
|
||||||
|
if api_key != "" do
|
||||||
|
case KeyValidation.validate_provider_key(api_key, provider_type) do
|
||||||
|
{:ok, _} ->
|
||||||
|
to_form(changeset)
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
changeset
|
||||||
|
|> Ecto.Changeset.add_error(:api_key, message)
|
||||||
|
|> to_form()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
to_form(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
# Store api_key separately since changeset encrypts it immediately
|
# Store api_key separately since changeset encrypts it immediately
|
||||||
{:noreply, assign(socket, form: form, pending_api_key: params["api_key"])}
|
{:noreply, assign(socket, form: form, pending_api_key: api_key)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
else: "Paste your key here"
|
else: "Paste your key here"
|
||||||
}
|
}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
phx-debounce="blur"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<%= if @live_action == :edit do %>
|
<%= if @live_action == :edit do %>
|
||||||
|
|||||||
@ -5,6 +5,14 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
alias Berrypod.Providers.Provider
|
alias Berrypod.Providers.Provider
|
||||||
alias Berrypod.Stripe.Setup, as: StripeSetup
|
alias Berrypod.Stripe.Setup, as: StripeSetup
|
||||||
|
|
||||||
|
# Steps in the guided flow (after account creation):
|
||||||
|
# :intro - explains what's needed
|
||||||
|
# :provider - connect print provider
|
||||||
|
# :stripe - connect Stripe payments
|
||||||
|
# :email - set up email provider
|
||||||
|
|
||||||
|
@guided_steps [:intro, :provider, :stripe, :email]
|
||||||
|
|
||||||
# ── Mount ──
|
# ── Mount ──
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -37,18 +45,15 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
logged_in? = get_user(socket) != nil
|
logged_in? = get_user(socket) != nil
|
||||||
provider_conn = Products.get_first_provider_connection()
|
provider_conn = Products.get_first_provider_connection()
|
||||||
|
|
||||||
current_step =
|
# Determine which guided step to show (for logged-in users)
|
||||||
cond do
|
guided_step = determine_guided_step(setup)
|
||||||
not logged_in? -> 1
|
|
||||||
not setup.provider_connected -> 2
|
|
||||||
true -> 3
|
|
||||||
end
|
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Set up your shop")
|
|> assign(:page_title, "Set up your shop")
|
||||||
|> assign(:setup, setup)
|
|> assign(:setup, setup)
|
||||||
|> assign(:logged_in?, logged_in?)
|
|> assign(:logged_in?, logged_in?)
|
||||||
|> assign(:current_step, current_step)
|
|> assign(:guided_step, guided_step)
|
||||||
|
|> assign(:guided_steps, @guided_steps)
|
||||||
# Secret gate
|
# Secret gate
|
||||||
|> assign(:require_secret?, Setup.require_setup_secret?())
|
|> assign(:require_secret?, Setup.require_setup_secret?())
|
||||||
|> assign(:secret_verified, false)
|
|> assign(:secret_verified, false)
|
||||||
@ -56,7 +61,10 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
# Account (card 1)
|
# Account (card 1)
|
||||||
|> assign(
|
|> assign(
|
||||||
:account_form,
|
:account_form,
|
||||||
to_form(%{"email" => "", "password" => "", "shop_name" => ""}, as: :account)
|
to_form(
|
||||||
|
%{"email" => "", "password" => "", "password_confirmation" => "", "shop_name" => ""},
|
||||||
|
as: :account
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# Provider (card 2)
|
# Provider (card 2)
|
||||||
|> assign(:providers, Provider.all())
|
|> assign(:providers, Provider.all())
|
||||||
@ -69,6 +77,30 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
|> assign(:stripe_connecting, false)
|
|> assign(:stripe_connecting, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Determine which guided step to show based on what's already configured
|
||||||
|
defp determine_guided_step(setup) do
|
||||||
|
cond do
|
||||||
|
# Show intro on first visit after account creation
|
||||||
|
not setup.provider_connected and not setup.stripe_connected and
|
||||||
|
not setup.email_configured ->
|
||||||
|
:intro
|
||||||
|
|
||||||
|
# Resume at first incomplete step
|
||||||
|
not setup.provider_connected ->
|
||||||
|
:provider
|
||||||
|
|
||||||
|
not setup.stripe_connected ->
|
||||||
|
:stripe
|
||||||
|
|
||||||
|
not setup.email_configured ->
|
||||||
|
:email
|
||||||
|
|
||||||
|
# All done - will redirect to dashboard
|
||||||
|
true ->
|
||||||
|
:email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# ── Events: Secret gate ──
|
# ── Events: Secret gate ──
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -80,19 +112,69 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── Events: Guided flow navigation ──
|
||||||
|
|
||||||
|
def handle_event("start_setup", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :guided_step, :provider)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("go_to_step", %{"step" => step}, socket) do
|
||||||
|
step = String.to_existing_atom(step)
|
||||||
|
|
||||||
|
if step in @guided_steps do
|
||||||
|
{:noreply, assign(socket, :guided_step, step)}
|
||||||
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("skip_step", _params, socket) do
|
||||||
|
next_step = next_guided_step(socket.assigns.guided_step)
|
||||||
|
handle_step_completion(socket, next_step)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("go_back", _params, socket) do
|
||||||
|
prev_step = prev_guided_step(socket.assigns.guided_step)
|
||||||
|
{:noreply, assign(socket, :guided_step, prev_step)}
|
||||||
|
end
|
||||||
|
|
||||||
# ── Events: Account ──
|
# ── Events: Account ──
|
||||||
|
|
||||||
|
def handle_event("validate_account", %{"account" => params}, socket) do
|
||||||
|
errors = validate_account_fields(params)
|
||||||
|
|
||||||
|
form =
|
||||||
|
to_form(params,
|
||||||
|
as: :account,
|
||||||
|
errors: errors,
|
||||||
|
action: if(errors != [], do: :validate)
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :account_form, form)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("create_account", %{"account" => params}, socket) do
|
def handle_event("create_account", %{"account" => params}, socket) do
|
||||||
email = params["email"]
|
email = params["email"]
|
||||||
password = params["password"]
|
password = params["password"]
|
||||||
|
password_confirmation = params["password_confirmation"]
|
||||||
shop_name = String.trim(params["shop_name"] || "")
|
shop_name = String.trim(params["shop_name"] || "")
|
||||||
|
|
||||||
cond do
|
errors = validate_account_fields(params)
|
||||||
shop_name == "" ->
|
|
||||||
{:noreply, put_flash(socket, :error, "Please enter a shop name")}
|
|
||||||
|
|
||||||
email == "" ->
|
cond do
|
||||||
{:noreply, put_flash(socket, :error, "Please enter your email address")}
|
errors != [] ->
|
||||||
|
form = to_form(params, as: :account, errors: errors, action: :validate)
|
||||||
|
{:noreply, assign(socket, :account_form, form)}
|
||||||
|
|
||||||
|
password != password_confirmation ->
|
||||||
|
form =
|
||||||
|
to_form(params,
|
||||||
|
as: :account,
|
||||||
|
errors: [password_confirmation: {"Passwords don't match", []}],
|
||||||
|
action: :validate
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :account_form, form)}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
Settings.put_setting("site_name", shop_name, "string")
|
Settings.put_setting("site_name", shop_name, "string")
|
||||||
@ -149,19 +231,13 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
{:ok, connection} ->
|
{:ok, connection} ->
|
||||||
setup = Setup.setup_status()
|
setup = Setup.setup_status()
|
||||||
|
|
||||||
if setup.setup_complete do
|
{:noreply,
|
||||||
{:noreply,
|
socket
|
||||||
socket
|
|> assign(:provider_connecting, false)
|
||||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
|> assign(:provider_conn, connection)
|
||||||
|> push_navigate(to: ~p"/admin")}
|
|> assign(:setup, setup)
|
||||||
else
|
|> assign(:guided_step, :stripe)
|
||||||
{:noreply,
|
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
||||||
socket
|
|
||||||
|> assign(:provider_connecting, false)
|
|
||||||
|> assign(:provider_conn, connection)
|
|
||||||
|> assign(:setup, setup)
|
|
||||||
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, :no_api_key} ->
|
{:error, :no_api_key} ->
|
||||||
form =
|
form =
|
||||||
@ -192,8 +268,46 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Validate provider key on blur for fast feedback
|
||||||
|
def handle_event("validate_provider", %{"provider" => %{"api_key" => api_key}}, socket) do
|
||||||
|
type = socket.assigns.selected_provider
|
||||||
|
|
||||||
|
form =
|
||||||
|
case KeyValidation.validate_provider_key(api_key, type) do
|
||||||
|
{:ok, _} ->
|
||||||
|
to_form(%{"api_key" => api_key}, as: :provider)
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
to_form(%{"api_key" => api_key},
|
||||||
|
as: :provider,
|
||||||
|
errors: [api_key: {message, []}],
|
||||||
|
action: :validate
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :provider_form, form)}
|
||||||
|
end
|
||||||
|
|
||||||
# ── Events: Stripe ──
|
# ── Events: Stripe ──
|
||||||
|
|
||||||
|
# Validate Stripe key on blur for fast feedback
|
||||||
|
def handle_event("validate_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
||||||
|
form =
|
||||||
|
case KeyValidation.validate_stripe_key(api_key) do
|
||||||
|
{:ok, _} ->
|
||||||
|
to_form(%{"api_key" => api_key}, as: :stripe)
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
to_form(%{"api_key" => api_key},
|
||||||
|
as: :stripe,
|
||||||
|
errors: [api_key: {message, []}],
|
||||||
|
action: :validate
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :stripe_form, form)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
||||||
case KeyValidation.validate_stripe_key(api_key) do
|
case KeyValidation.validate_stripe_key(api_key) do
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
@ -213,18 +327,12 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
{:ok, _result} ->
|
{:ok, _result} ->
|
||||||
setup = Setup.setup_status()
|
setup = Setup.setup_status()
|
||||||
|
|
||||||
if setup.setup_complete do
|
{:noreply,
|
||||||
{:noreply,
|
socket
|
||||||
socket
|
|> assign(:stripe_connecting, false)
|
||||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
|> assign(:setup, setup)
|
||||||
|> push_navigate(to: ~p"/admin")}
|
|> assign(:guided_step, :email)
|
||||||
else
|
|> put_flash(:info, "Stripe connected")}
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:stripe_connecting, false)
|
|
||||||
|> assign(:setup, setup)
|
|
||||||
|> put_flash(:info, "Stripe connected")}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
form =
|
form =
|
||||||
@ -242,6 +350,37 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── Navigation helpers ──
|
||||||
|
|
||||||
|
defp next_guided_step(current) do
|
||||||
|
case current do
|
||||||
|
:intro -> :provider
|
||||||
|
:provider -> :stripe
|
||||||
|
:stripe -> :email
|
||||||
|
:email -> :done
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prev_guided_step(current) do
|
||||||
|
case current do
|
||||||
|
:provider -> :intro
|
||||||
|
:stripe -> :provider
|
||||||
|
:email -> :stripe
|
||||||
|
_ -> current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_step_completion(socket, :done) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Setup complete! Here's your launch checklist.")
|
||||||
|
|> push_navigate(to: ~p"/admin")}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_step_completion(socket, next_step) do
|
||||||
|
{:noreply, assign(socket, :guided_step, next_step)}
|
||||||
|
end
|
||||||
|
|
||||||
# ── Render ──
|
# ── Render ──
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -288,16 +427,16 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%!-- All three setup cards --%>
|
<%!-- Account creation (before login) --%>
|
||||||
<div class="setup-sections">
|
<div :if={not @logged_in?} class="setup-sections">
|
||||||
<.section_card
|
<.section_card
|
||||||
title="Set up your account"
|
title="Set up your account"
|
||||||
number={1}
|
number={1}
|
||||||
done={@logged_in?}
|
done={false}
|
||||||
summary={account_summary(assigns)}
|
summary={nil}
|
||||||
>
|
>
|
||||||
<p class="setup-hint">Name your shop and 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">
|
<.form for={@account_form} phx-submit="create_account" phx-change="validate_account">
|
||||||
<.input
|
<.input
|
||||||
field={@account_form[:shop_name]}
|
field={@account_form[:shop_name]}
|
||||||
type="text"
|
type="text"
|
||||||
@ -305,8 +444,9 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
placeholder="e.g. Acme Prints"
|
placeholder="e.g. Acme Prints"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
phx-mounted={@current_step == 1 && JS.focus()}
|
phx-mounted={JS.focus()}
|
||||||
/>
|
/>
|
||||||
|
<p class="setup-field-note">You can change this later</p>
|
||||||
<.input
|
<.input
|
||||||
field={@account_form[:email]}
|
field={@account_form[:email]}
|
||||||
type="email"
|
type="email"
|
||||||
@ -322,48 +462,149 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<.input
|
||||||
|
field={@account_form[:password_confirmation]}
|
||||||
|
type="password"
|
||||||
|
label="Confirm password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<div class="setup-actions">
|
<div class="setup-actions">
|
||||||
<.button phx-disable-with="Creating account...">Create account</.button>
|
<.button phx-disable-with="Creating account...">Create account</.button>
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
</.section_card>
|
</.section_card>
|
||||||
|
|
||||||
<.section_card
|
|
||||||
title="Connect a print provider"
|
|
||||||
number={2}
|
|
||||||
done={@setup.provider_connected}
|
|
||||||
summary={provider_summary(assigns)}
|
|
||||||
>
|
|
||||||
<.provider_section
|
|
||||||
providers={@providers}
|
|
||||||
selected={@selected_provider}
|
|
||||||
form={@provider_form}
|
|
||||||
connecting={@provider_connecting}
|
|
||||||
/>
|
|
||||||
</.section_card>
|
|
||||||
|
|
||||||
<.section_card
|
|
||||||
title="Connect payments"
|
|
||||||
number={3}
|
|
||||||
done={@setup.stripe_connected}
|
|
||||||
summary={stripe_summary(assigns)}
|
|
||||||
>
|
|
||||||
<.stripe_section
|
|
||||||
form={@stripe_form}
|
|
||||||
connecting={@stripe_connecting}
|
|
||||||
focus={@current_step == 3}
|
|
||||||
/>
|
|
||||||
</.section_card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- All done --%>
|
<%!-- Guided setup flow (after login) --%>
|
||||||
<div :if={@setup.setup_complete} class="setup-complete">
|
<div :if={@logged_in?} class="setup-guided">
|
||||||
<.icon name="hero-check-badge" class="setup-complete-icon" />
|
<%!-- Progress bar --%>
|
||||||
<h2>You're all set</h2>
|
<.progress_bar
|
||||||
<p>Head to the dashboard to sync products, customise your theme, and go live.</p>
|
current_step={@guided_step}
|
||||||
<.link navigate={~p"/admin"} class="admin-btn admin-btn-primary">
|
setup={@setup}
|
||||||
Go to dashboard <span aria-hidden="true">→</span>
|
/>
|
||||||
</.link>
|
|
||||||
|
<%!-- Intro screen --%>
|
||||||
|
<div :if={@guided_step == :intro} class="setup-intro">
|
||||||
|
<div class="setup-intro-content">
|
||||||
|
<p class="setup-intro-lead">
|
||||||
|
Berrypod connects your print-on-demand products to your own online shop.
|
||||||
|
To get fully set up, you'll need three things:
|
||||||
|
</p>
|
||||||
|
<ul class="setup-intro-list">
|
||||||
|
<li>
|
||||||
|
<.icon name="hero-cube" class="setup-intro-icon" />
|
||||||
|
<div>
|
||||||
|
<strong>A print provider account</strong>
|
||||||
|
<span>(like Printify or Printful) to make and ship your products</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.icon name="hero-credit-card" class="setup-intro-icon" />
|
||||||
|
<div>
|
||||||
|
<strong>A Stripe account</strong>
|
||||||
|
<span>to accept payments from customers</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<.icon name="hero-envelope" class="setup-intro-icon" />
|
||||||
|
<div>
|
||||||
|
<strong>An email provider account</strong>
|
||||||
|
<span>so your shop can send order confirmations and shipping updates</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="setup-intro-note">
|
||||||
|
Don't worry if you don't have all of these yet — you can skip any step and set it up later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="setup-actions setup-actions-center">
|
||||||
|
<.button phx-click="start_setup">
|
||||||
|
Let's get started <span aria-hidden="true">→</span>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- Step 1: Provider --%>
|
||||||
|
<div :if={@guided_step == :provider} class="setup-step">
|
||||||
|
<.step_header
|
||||||
|
number={1}
|
||||||
|
title="Connect a print provider"
|
||||||
|
done={@setup.provider_connected}
|
||||||
|
/>
|
||||||
|
<div class="setup-step-body">
|
||||||
|
<.provider_section
|
||||||
|
providers={@providers}
|
||||||
|
selected={@selected_provider}
|
||||||
|
form={@provider_form}
|
||||||
|
connecting={@provider_connecting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<.step_nav
|
||||||
|
step={:provider}
|
||||||
|
can_skip={true}
|
||||||
|
skip_note="You won't be able to import products until you connect a provider."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- Step 2: Stripe --%>
|
||||||
|
<div :if={@guided_step == :stripe} class="setup-step">
|
||||||
|
<.step_header
|
||||||
|
number={2}
|
||||||
|
title="Connect Stripe for payments"
|
||||||
|
done={@setup.stripe_connected}
|
||||||
|
/>
|
||||||
|
<div class="setup-step-body">
|
||||||
|
<.stripe_section
|
||||||
|
form={@stripe_form}
|
||||||
|
connecting={@stripe_connecting}
|
||||||
|
focus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<.step_nav
|
||||||
|
step={:stripe}
|
||||||
|
can_skip={true}
|
||||||
|
skip_note="Customers won't be able to checkout until you connect Stripe."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- Step 3: Email --%>
|
||||||
|
<div :if={@guided_step == :email} class="setup-step">
|
||||||
|
<.step_header
|
||||||
|
number={3}
|
||||||
|
title="Set up email"
|
||||||
|
done={@setup.email_configured}
|
||||||
|
/>
|
||||||
|
<div class="setup-step-body">
|
||||||
|
<p class="setup-hint">
|
||||||
|
Configure an email provider so your shop can send order confirmations,
|
||||||
|
shipping updates, and newsletters.
|
||||||
|
</p>
|
||||||
|
<%= if @setup.email_configured do %>
|
||||||
|
<div class="setup-step-done">
|
||||||
|
<.icon name="hero-check-circle" class="setup-step-done-icon" />
|
||||||
|
<p>Email is already configured.</p>
|
||||||
|
</div>
|
||||||
|
<div class="setup-actions">
|
||||||
|
<.button phx-click="skip_step">
|
||||||
|
Continue <span aria-hidden="true">→</span>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="setup-hint">
|
||||||
|
<.link navigate={~p"/admin/settings/email"} class="setup-link">
|
||||||
|
Set up email in settings <span aria-hidden="true">→</span>
|
||||||
|
</.link>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<.step_nav
|
||||||
|
step={:email}
|
||||||
|
can_skip={true}
|
||||||
|
skip_note="Order confirmations and shipping updates won't be sent."
|
||||||
|
finish_button={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@ -375,7 +616,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
attr :title, :string, required: true
|
attr :title, :string, required: true
|
||||||
attr :number, :integer, required: true
|
attr :number, :integer, required: true
|
||||||
attr :done, :boolean, required: true
|
attr :done, :boolean, required: true
|
||||||
attr :summary, :string, default: nil
|
attr :summary, :any, default: nil
|
||||||
|
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
@ -404,6 +645,120 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── Progress bar component ──
|
||||||
|
|
||||||
|
attr :current_step, :atom, required: true
|
||||||
|
attr :setup, :map, required: true
|
||||||
|
|
||||||
|
defp progress_bar(assigns) do
|
||||||
|
steps = [
|
||||||
|
%{key: :provider, label: "Provider", done: assigns.setup.provider_connected},
|
||||||
|
%{key: :stripe, label: "Payments", done: assigns.setup.stripe_connected},
|
||||||
|
%{key: :email, label: "Email", done: assigns.setup.email_configured}
|
||||||
|
]
|
||||||
|
|
||||||
|
current_index =
|
||||||
|
case assigns.current_step do
|
||||||
|
:intro -> -1
|
||||||
|
:provider -> 0
|
||||||
|
:stripe -> 1
|
||||||
|
:email -> 2
|
||||||
|
_ -> -1
|
||||||
|
end
|
||||||
|
|
||||||
|
assigns = assign(assigns, steps: steps, current_index: current_index)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<nav class="setup-progress" aria-label="Setup progress">
|
||||||
|
<ol class="setup-progress-list">
|
||||||
|
<%= for {step, index} <- Enum.with_index(@steps) do %>
|
||||||
|
<li class={[
|
||||||
|
"setup-progress-item",
|
||||||
|
step.done && "setup-progress-done",
|
||||||
|
index == @current_index && "setup-progress-current"
|
||||||
|
]}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="go_to_step"
|
||||||
|
phx-value-step={step.key}
|
||||||
|
class="setup-progress-button"
|
||||||
|
aria-current={index == @current_index && "step"}
|
||||||
|
disabled={@current_step == :intro}
|
||||||
|
>
|
||||||
|
<span class="setup-progress-indicator">
|
||||||
|
<%= if step.done do %>
|
||||||
|
<.icon name="hero-check-mini" class="size-4" />
|
||||||
|
<% else %>
|
||||||
|
{index + 1}
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<span class="setup-progress-label">{step.label}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Step header component ──
|
||||||
|
|
||||||
|
attr :number, :integer, required: true
|
||||||
|
attr :title, :string, required: true
|
||||||
|
attr :done, :boolean, default: false
|
||||||
|
|
||||||
|
defp step_header(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="setup-step-header">
|
||||||
|
<span class={["setup-step-number", @done && "setup-step-number-done"]}>
|
||||||
|
<%= if @done do %>
|
||||||
|
<.icon name="hero-check-mini" class="size-5" />
|
||||||
|
<% else %>
|
||||||
|
{@number}
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<h2 class="setup-step-title">{@title}</h2>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Step navigation component ──
|
||||||
|
|
||||||
|
attr :step, :atom, required: true
|
||||||
|
attr :can_skip, :boolean, default: false
|
||||||
|
attr :skip_note, :string, default: nil
|
||||||
|
attr :finish_button, :boolean, default: false
|
||||||
|
|
||||||
|
defp step_nav(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="setup-step-nav">
|
||||||
|
<div class="setup-step-nav-left">
|
||||||
|
<%= if @step != :provider do %>
|
||||||
|
<button type="button" phx-click="go_back" class="setup-step-back">
|
||||||
|
<.icon name="hero-arrow-left-mini" class="size-4" /> Back
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="setup-step-nav-right">
|
||||||
|
<%= if @can_skip do %>
|
||||||
|
<div class="setup-step-skip">
|
||||||
|
<button type="button" phx-click="skip_step" class="setup-step-skip-btn">
|
||||||
|
<%= if @finish_button do %>
|
||||||
|
Finish setup
|
||||||
|
<% else %>
|
||||||
|
Skip for now
|
||||||
|
<% end %>
|
||||||
|
</button>
|
||||||
|
<%= if @skip_note do %>
|
||||||
|
<p class="setup-step-skip-note">{@skip_note}</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
# ── Provider section ──
|
# ── Provider section ──
|
||||||
|
|
||||||
attr :providers, :list, required: true
|
attr :providers, :list, required: true
|
||||||
@ -437,13 +792,14 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
</.external_link>
|
</.external_link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.form for={@form} phx-submit="connect_provider">
|
<.form for={@form} phx-submit="connect_provider" phx-change="validate_provider">
|
||||||
<.input
|
<.input
|
||||||
field={@form[:api_key]}
|
field={@form[:api_key]}
|
||||||
type="text"
|
type="text"
|
||||||
label="API token"
|
label="API token"
|
||||||
placeholder="Paste your token here"
|
placeholder="Paste your token here"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
phx-debounce="blur"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="setup-actions">
|
<div class="setup-actions">
|
||||||
@ -473,7 +829,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
</.external_link>
|
</.external_link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.form for={@form} phx-submit="connect_stripe">
|
<.form for={@form} phx-submit="connect_stripe" phx-change="validate_stripe">
|
||||||
<.input
|
<.input
|
||||||
field={@form[:api_key]}
|
field={@form[:api_key]}
|
||||||
type="text"
|
type="text"
|
||||||
@ -481,6 +837,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="sk_test_... or sk_live_..."
|
placeholder="sk_test_... or sk_live_..."
|
||||||
phx-mounted={@focus && JS.focus()}
|
phx-mounted={@focus && JS.focus()}
|
||||||
|
phx-debounce="blur"
|
||||||
/>
|
/>
|
||||||
<div class="setup-actions">
|
<div class="setup-actions">
|
||||||
<.button phx-disable-with="Connecting...">
|
<.button phx-disable-with="Connecting...">
|
||||||
@ -539,4 +896,49 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Live validation for account creation form
|
||||||
|
defp validate_account_fields(params) do
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
shop_name = String.trim(params["shop_name"] || "")
|
||||||
|
email = String.trim(params["email"] || "")
|
||||||
|
password = params["password"] || ""
|
||||||
|
password_confirmation = params["password_confirmation"] || ""
|
||||||
|
|
||||||
|
errors =
|
||||||
|
if shop_name == "" and params["shop_name"] != "" do
|
||||||
|
[{:shop_name, {"Shop name can't be blank", []}} | errors]
|
||||||
|
else
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
|
||||||
|
errors =
|
||||||
|
if email != "" and not valid_email?(email) do
|
||||||
|
[{:email, {"Please enter a valid email address", []}} | errors]
|
||||||
|
else
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
|
||||||
|
errors =
|
||||||
|
if password != "" and String.length(password) < 12 do
|
||||||
|
[{:password, {"Password must be at least 12 characters", []}} | errors]
|
||||||
|
else
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
|
||||||
|
errors =
|
||||||
|
if password_confirmation != "" and password != password_confirmation do
|
||||||
|
[{:password_confirmation, {"Passwords don't match", []}} | errors]
|
||||||
|
else
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
|
||||||
|
Enum.reverse(errors)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp valid_email?(email) do
|
||||||
|
# Basic email format check
|
||||||
|
String.match?(email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -61,15 +61,17 @@ defmodule Berrypod.KeyValidationTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validate_provider_key/2" do
|
describe "validate_provider_key/2 - Printify" do
|
||||||
test "accepts reasonable key" do
|
@printify_uuid "12345678-1234-1234-1234-123456789012"
|
||||||
assert {:ok, "abcdef1234567890abcdef"} =
|
|
||||||
KeyValidation.validate_provider_key("abcdef1234567890abcdef", "printify")
|
test "accepts valid UUID format key" do
|
||||||
|
assert {:ok, @printify_uuid} =
|
||||||
|
KeyValidation.validate_provider_key(@printify_uuid, "printify")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "strips whitespace" do
|
test "strips whitespace" do
|
||||||
assert {:ok, "abcdef1234567890abcdef"} =
|
assert {:ok, @printify_uuid} =
|
||||||
KeyValidation.validate_provider_key(" abcdef1234567890abcdef ", "printful")
|
KeyValidation.validate_provider_key(" #{@printify_uuid} ", "printify")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rejects empty key" do
|
test "rejects empty key" do
|
||||||
@ -87,9 +89,41 @@ defmodule Berrypod.KeyValidationTest do
|
|||||||
assert msg =~ "too short"
|
assert msg =~ "too short"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rejects too-long key" do
|
||||||
|
long_key = String.duplicate("a", 50)
|
||||||
|
assert {:error, msg} = KeyValidation.validate_provider_key(long_key, "printify")
|
||||||
|
assert msg =~ "too long"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "gives helpful error for non-UUID format" do
|
||||||
|
assert {:error, msg} =
|
||||||
|
KeyValidation.validate_provider_key("12345678123412341234123456789012", "printify")
|
||||||
|
|
||||||
|
assert msg =~ "UUID format"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "validate_provider_key/2 - Printful" do
|
||||||
|
test "accepts reasonable length key" do
|
||||||
|
key = String.duplicate("x", 40)
|
||||||
|
assert {:ok, ^key} = KeyValidation.validate_provider_key(key, "printful")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "strips whitespace" do
|
||||||
|
key = String.duplicate("x", 40)
|
||||||
|
assert {:ok, ^key} = KeyValidation.validate_provider_key(" #{key} ", "printful")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects too-short key" do
|
||||||
|
assert {:error, msg} = KeyValidation.validate_provider_key("short", "printful")
|
||||||
|
assert msg =~ "too short"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "validate_provider_key/2 - generic" do
|
||||||
test "works without provider type" do
|
test "works without provider type" do
|
||||||
assert {:ok, "abcdef1234567890abcdef"} =
|
key = String.duplicate("x", 20)
|
||||||
KeyValidation.validate_provider_key("abcdef1234567890abcdef")
|
assert {:ok, ^key} = KeyValidation.validate_provider_key(key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -28,9 +28,9 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
|||||||
|
|
||||||
assert html =~ "Launch checklist"
|
assert html =~ "Launch checklist"
|
||||||
assert html =~ "Connect a print provider"
|
assert html =~ "Connect a print provider"
|
||||||
assert html =~ "Connect Stripe"
|
assert html =~ "Connect Stripe for payments"
|
||||||
assert html =~ "Sync your products"
|
assert html =~ "Import products"
|
||||||
assert html =~ "Customise your theme"
|
assert html =~ "Customise your shop"
|
||||||
assert html =~ "Go live"
|
assert html =~ "Go live"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -43,16 +43,16 @@ defmodule BerrypodWeb.Admin.DashboardTest do
|
|||||||
|
|
||||||
test "collapse and expand checklist", %{conn: conn} do
|
test "collapse and expand checklist", %{conn: conn} do
|
||||||
{:ok, view, html} = live(conn, ~p"/admin")
|
{:ok, view, html} = live(conn, ~p"/admin")
|
||||||
assert html =~ "Sync your products"
|
assert html =~ "Import products"
|
||||||
|
|
||||||
# Collapse
|
# Collapse
|
||||||
html = render_click(view, "toggle_checklist")
|
html = render_click(view, "toggle_checklist")
|
||||||
refute html =~ "Sync your products"
|
refute html =~ "Import products"
|
||||||
assert html =~ "Launch checklist"
|
assert html =~ "Launch checklist"
|
||||||
|
|
||||||
# Expand
|
# Expand
|
||||||
html = render_click(view, "toggle_checklist")
|
html = render_click(view, "toggle_checklist")
|
||||||
assert html =~ "Sync your products"
|
assert html =~ "Import products"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "go live button works", %{conn: conn} do
|
test "go live button works", %{conn: conn} do
|
||||||
|
|||||||
@ -174,11 +174,14 @@ defmodule BerrypodWeb.Admin.ProvidersTest do
|
|||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
|
{:ok, view, _html} = live(conn, ~p"/admin/providers/new")
|
||||||
|
|
||||||
|
# Printify uses 36-character UUID format keys
|
||||||
|
valid_uuid_key = "12345678-1234-1234-1234-123456789012"
|
||||||
|
|
||||||
{:ok, _view, html} =
|
{:ok, _view, html} =
|
||||||
view
|
view
|
||||||
|> form("#provider-form", %{
|
|> form("#provider-form", %{
|
||||||
"provider_connection" => %{
|
"provider_connection" => %{
|
||||||
"api_key" => "test_key_123"
|
"api_key" => valid_uuid_key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|||||||
@ -57,12 +57,13 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "fresh install (no admin)" do
|
describe "fresh install (no admin)" do
|
||||||
test "shows all three cards with email form active", %{conn: conn} do
|
test "shows only account card (simplified setup)", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
{:ok, _view, html} = live(conn, ~p"/setup")
|
||||||
|
|
||||||
assert html =~ "Set up your account"
|
assert html =~ "Set up your account"
|
||||||
assert html =~ "Connect a print provider"
|
# Provider and Stripe cards are hidden until after account creation
|
||||||
assert html =~ "Connect payments"
|
refute html =~ "Connect a print provider"
|
||||||
|
refute html =~ "Connect payments"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creating account auto-confirms and redirects to login", %{conn: conn} do
|
test "creating account auto-confirms and redirects to login", %{conn: conn} do
|
||||||
@ -73,7 +74,8 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
|||||||
account: %{
|
account: %{
|
||||||
shop_name: "Test Shop",
|
shop_name: "Test Shop",
|
||||||
email: "admin@example.com",
|
email: "admin@example.com",
|
||||||
password: "valid_password_123"
|
password: "valid_password_123",
|
||||||
|
password_confirmation: "valid_password_123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
@ -87,26 +89,31 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
|||||||
describe "configure (logged in)" do
|
describe "configure (logged in)" do
|
||||||
setup :register_and_log_in_user
|
setup :register_and_log_in_user
|
||||||
|
|
||||||
test "shows provider and stripe steps", %{conn: conn, user: user} do
|
test "shows guided setup flow", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
{:ok, _view, html} = live(conn, ~p"/setup")
|
||||||
|
|
||||||
assert html =~ user.email
|
# Guided flow shows progress bar
|
||||||
assert html =~ "Connect a print provider"
|
assert html =~ "Setup progress"
|
||||||
assert html =~ "Connect payments"
|
# Shows one of the setup steps or intro
|
||||||
|
assert html =~ "Connect" or html =~ "Let's get started"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows provider cards", %{conn: conn} do
|
test "clicking start shows provider step", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
{:ok, view, _html} = live(conn, ~p"/setup")
|
||||||
|
|
||||||
|
html = render_click(view, "start_setup")
|
||||||
|
|
||||||
|
assert html =~ "Connect a print provider"
|
||||||
assert html =~ "Printify"
|
assert html =~ "Printify"
|
||||||
assert html =~ "Printful"
|
assert html =~ "Printful"
|
||||||
assert html =~ "Gelato"
|
|
||||||
assert html =~ "Coming soon"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "selecting a provider shows the API key form", %{conn: conn} do
|
test "selecting a provider shows the API key form", %{conn: conn} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/setup")
|
{:ok, view, _html} = live(conn, ~p"/setup")
|
||||||
|
|
||||||
|
# First go to provider step
|
||||||
|
render_click(view, "start_setup")
|
||||||
|
|
||||||
html =
|
html =
|
||||||
view
|
view
|
||||||
|> form(~s(form[phx-change="select_provider"]), %{provider_select: %{type: "printify"}})
|
|> form(~s(form[phx-change="select_provider"]), %{provider_select: %{type: "printify"}})
|
||||||
@ -116,11 +123,15 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
|||||||
assert html =~ "Printify"
|
assert html =~ "Printify"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows stripe form", %{conn: conn} do
|
test "skipping provider step shows stripe step", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/setup")
|
{:ok, view, _html} = live(conn, ~p"/setup")
|
||||||
|
|
||||||
|
# Start and skip provider
|
||||||
|
render_click(view, "start_setup")
|
||||||
|
html = render_click(view, "skip_step")
|
||||||
|
|
||||||
assert html =~ "Secret key"
|
assert html =~ "Secret key"
|
||||||
assert html =~ "Connect payments"
|
assert html =~ "Connect Stripe"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user