From 7df8a49e5f1d2f3f014e0f897d5332be17608b6f Mon Sep 17 00:00:00 2001 From: abhishek Date: Wed, 2 Sep 2026 11:26:00 +0530 Subject: [PATCH] setup onboarding page --- src/App.tsx | 6 +- src/features/onboarding/OnboardingGate.tsx | 51 +++ src/features/onboarding/OnboardingPage.tsx | 344 ++++++++++++++++++ src/features/onboarding/Stepper.tsx | 96 +++++ .../onboarding/onboardingState.test.ts | 100 +++++ src/features/onboarding/onboardingState.ts | 201 ++++++++++ .../onboarding/steps/CatalogueStep.tsx | 127 +++++++ .../onboarding/steps/DeliveryStep.tsx | 197 ++++++++++ src/features/onboarding/steps/DoneStep.tsx | 120 ++++++ .../onboarding/steps/InventoryStep.tsx | 143 ++++++++ src/features/onboarding/steps/ReviewStep.tsx | 152 ++++++++ .../onboarding/steps/StoreInfoStep.tsx | 330 +++++++++++++++++ src/features/onboarding/steps/WelcomeStep.tsx | 149 ++++++++ src/features/onboarding/validation.test.ts | 122 +++++++ src/features/onboarding/validation.ts | 121 ++++++ src/features/setup/SetupPage.tsx | 302 --------------- src/features/setup/SetupTour.tsx | 11 +- src/features/setup/StoreAdminSetupPage.tsx | 35 -- src/features/setup/StoreAdminTour.tsx | 28 -- src/features/setup/StoreUserSetupPage.tsx | 35 -- src/features/setup/StoreUserTour.tsx | 1 - src/features/store-admin/StoreAdminShell.tsx | 4 +- src/index.css | 232 ++++++++++++ 23 files changed, 2490 insertions(+), 417 deletions(-) create mode 100644 src/features/onboarding/OnboardingGate.tsx create mode 100644 src/features/onboarding/OnboardingPage.tsx create mode 100644 src/features/onboarding/Stepper.tsx create mode 100644 src/features/onboarding/onboardingState.test.ts create mode 100644 src/features/onboarding/onboardingState.ts create mode 100644 src/features/onboarding/steps/CatalogueStep.tsx create mode 100644 src/features/onboarding/steps/DeliveryStep.tsx create mode 100644 src/features/onboarding/steps/DoneStep.tsx create mode 100644 src/features/onboarding/steps/InventoryStep.tsx create mode 100644 src/features/onboarding/steps/ReviewStep.tsx create mode 100644 src/features/onboarding/steps/StoreInfoStep.tsx create mode 100644 src/features/onboarding/steps/WelcomeStep.tsx create mode 100644 src/features/onboarding/validation.test.ts create mode 100644 src/features/onboarding/validation.ts delete mode 100644 src/features/setup/SetupPage.tsx delete mode 100644 src/features/setup/StoreAdminSetupPage.tsx delete mode 100644 src/features/setup/StoreAdminTour.tsx delete mode 100644 src/features/setup/StoreUserSetupPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 3740703..5466f45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,8 +43,7 @@ const UsersPage = named('UsersPage', () => import('@/features/store-admin/pages/ const TerminalsPage = named('TerminalsPage', () => import('@/features/store-admin/pages/TerminalsPage')); const AdminUploadsPage = named('UploadsPage', () => import('@/features/store-admin/pages/UploadsPage')); const ShopProfilePage = named('ShopProfilePage', () => import('@/features/store-admin/pages/ShopProfilePage')); -const StoreAdminSetupPage = named('StoreAdminSetupPage', () => import('@/features/setup/StoreAdminSetupPage')); -const StoreUserSetupPage = named('StoreUserSetupPage', () => import('@/features/setup/StoreUserSetupPage')); +const OnboardingPage = named('OnboardingPage', () => import('@/features/onboarding/OnboardingPage')); /* The Store user workspace reuses the merchant's four pages, pinned to one branch by `BranchScopeProvider pin=`. Only what a shop does differently is @@ -128,7 +127,7 @@ export function App() { } /> } /> } /> - } /> + } /> {/* Catches `/admin/dashboard` and anything else that does not resolve. Without this, an unknown sub-path escapes to the global `*`, which redirects to this role's HOME_ROUTE — and if that is itself an @@ -160,7 +159,6 @@ export function App() { } /> } /> } /> - } /> } /> } /> diff --git a/src/features/onboarding/OnboardingGate.tsx b/src/features/onboarding/OnboardingGate.tsx new file mode 100644 index 0000000..e6ca607 --- /dev/null +++ b/src/features/onboarding/OnboardingGate.tsx @@ -0,0 +1,51 @@ +import { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/auth/AuthContext'; +import { useBranchScope } from '@/features/store-admin/BranchScope'; +import { useOwnTenant } from '@/queries/hooks'; +import { readOnboarding } from './onboardingState'; + +/** + * Sends a first-time merchant to setup instead of the dashboard. + * + * Renders nothing — it is a decision, not a screen, and it makes that decision + * once. Three rules keep it from becoming a trap: + * + * - **Only once.** After the first redirect, `started` is set and nobody is + * ever sent again. Somebody who leaves setup has left it. + * - **Only from the landing page.** A merchant who deep-links to Sales, or is + * already reading Inventory, is not hauled away from what they opened. + * - **Only when there is something to do.** A shop whose profile is already + * filled in is not a first-time user, however new the account. + * + * The redirect waits for the shop record. Judging "incomplete" while the query + * is still in flight would redirect every merchant on every first paint. + */ +export function OnboardingGate() { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const { user } = useAuth(); + const { tenantid } = useBranchScope(); + const shop = useOwnTenant(tenantid || undefined); + + useEffect(() => { + if (!user?.userid || !tenantid || shop.isLoading || !shop.data) return; + if (pathname !== '/admin/console') return; + + const state = readOnboarding(user.userid, tenantid); + if (state.started || state.finished) return; + + // "Set up" means the shop can be found and described. Products and stock + // come later and are not a reason to interrupt somebody. + const record = shop.data as unknown as Record; + const hasBasics = + String(record['tenantname'] ?? '').trim() !== '' && + String(record['address'] ?? '').trim() !== '' && + String(record['primarycontact'] ?? '').trim() !== ''; + if (hasBasics) return; + + navigate('/admin/onboarding', { replace: true }); + }, [user?.userid, tenantid, shop.isLoading, shop.data, pathname, navigate]); + + return null; +} diff --git a/src/features/onboarding/OnboardingPage.tsx b/src/features/onboarding/OnboardingPage.tsx new file mode 100644 index 0000000..dec1e38 --- /dev/null +++ b/src/features/onboarding/OnboardingPage.tsx @@ -0,0 +1,344 @@ +/** + * Store setup, as a page inside the console rather than a modal over it. + * + * It renders through the shell's ``, so the existing top navigation, + * branding and account menu stay exactly where they are and the flow reads as + * part of the product. No sidebar, no second navigation: the progress belongs + * under the header, and the header is the one the rest of the console uses. + * + * ── What is stored where ──────────────────────────────────────────────────── + * + * Answers go to Fiesta as each step is completed — the shop record through + * `tenants/updatetenant`, delivery through `tenants/updatelocation` — so setup + * is not a pile of state that only becomes real at the end. Position and + * half-typed drafts live in the browser, because an unsent draft is not + * something to publish to a shop's record on every keystroke. + * + * ── Why the steps do not block ────────────────────────────────────────────── + * + * Store details are required: nothing else is meaningful without a name and an + * address. Catalogue and inventory are not — a shop whose product list is not + * ready yet should be able to finish and come back, and a wall there is how + * somebody abandons setup on their first afternoon. + */ + +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '@astryxdesign/core/Button'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { VStack } from '@astryxdesign/core/VStack'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; +import { errorMessage } from '@/api/client'; +import { tenantsApi } from '@/api/tenants'; +import { useAuth } from '@/auth/AuthContext'; +import { PageBody } from '@/components/PageBody'; +import { useBranchScope } from '@/features/store-admin/BranchScope'; +import { useLocationProducts, useOwnTenant } from '@/queries/hooks'; +import { Stepper } from './Stepper'; +import { + completeStep, + progressOf, + readOnboarding, + resumeStep, + skipStep, + writeOnboarding, + type OnboardingState, + type StepId, +} from './onboardingState'; +import { isValid, normalisePhone, validateDelivery, validateStoreInfo, type Errors } from './validation'; +import { WelcomeStep } from './steps/WelcomeStep'; +import { StoreInfoStep } from './steps/StoreInfoStep'; +import { CatalogueStep } from './steps/CatalogueStep'; +import { InventoryStep } from './steps/InventoryStep'; +import { DeliveryStep } from './steps/DeliveryStep'; +import { ReviewStep } from './steps/ReviewStep'; +import { DoneStep } from './steps/DoneStep'; + +const HEADINGS: Record = { + welcome: { title: 'Set up your store', blurb: '' }, + store: { title: "Let's set up your store", blurb: 'Tell us a few details about your shop.' }, + catalogue: { + title: 'How would you like to add your products?', + blurb: 'Choose whichever matches what you already have.', + }, + inventory: { + title: "Let's set up your stock", + blurb: 'Keep what customers see in step with what is on your shelf.', + }, + delivery: { + title: 'Set up your delivery', + blurb: 'Tell us how you would like to get orders to customers.', + }, + review: { + title: 'Review your setup', + blurb: 'Check everything over before you finish.', + }, + done: { title: 'Setup complete', blurb: '' }, +}; + +/** Forward order. `done` has no next; `welcome` has no back. */ +const ORDER: StepId[] = ['welcome', 'store', 'catalogue', 'inventory', 'delivery', 'review', 'done']; + +export function OnboardingPage() { + const navigate = useNavigate(); + const { user } = useAuth(); + const { tenantid, current } = useBranchScope(); + const userid = user?.userid ?? 0; + + const shop = useOwnTenant(tenantid || undefined); + const products = useLocationProducts(tenantid || undefined, undefined, 0, { allBranches: true }); + + const [state, setState] = useState(() => readOnboarding(userid, tenantid)); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + /** + * The shop's own record wins over an empty draft. + * + * Onboarding already collected a name, phone and address, so presenting blank + * fields would ask a merchant to type what the platform is holding two + * screens away. Only fields the draft has not touched are filled, so a + * half-typed answer is never overwritten by a refetch. + */ + useEffect(() => { + const record = shop.data as unknown as Record | undefined; + if (!record) return; + setState((prev) => { + const seeded = { ...prev.store }; + let changed = false; + for (const key of [ + 'tenantname', 'tenantimage', 'primaryemail', 'primarycontact', + 'address', 'city', 'postcode', 'state', 'tenanttype', + ] as const) { + if (seeded[key] === '' && typeof record[key] === 'string' && record[key]) { + seeded[key] = record[key] as string; + changed = true; + } + } + return changed ? { ...prev, store: seeded } : prev; + }); + }, [shop.data]); + + const productCount = (products.data ?? []).length; + const step = resumeStep(state); + const { done, total } = progressOf(state); + + function persist(next: OnboardingState) { + setState(writeOnboarding(userid, tenantid, next)); + setErrors({}); + setSaveError(null); + } + + function goTo(next: StepId) { + persist({ ...state, step: next, started: true }); + } + + const nextOf = (id: StepId) => ORDER[Math.min(ORDER.indexOf(id) + 1, ORDER.length - 1)] as StepId; + const backOf = (id: StepId) => ORDER[Math.max(ORDER.indexOf(id) - 1, 0)] as StepId; + + /** Save what this step owns, then advance. Nothing advances on a failed write. */ + async function saveAndContinue() { + setSaveError(null); + + if (step === 'store') { + const found = validateStoreInfo(state.store); + setErrors(found); + if (!isValid(found)) return; + setSaving(true); + try { + await tenantsApi.updateProfile({ + tenantid, + tenantname: state.store.tenantname.trim(), + tenantimage: state.store.tenantimage.trim(), + primaryemail: state.store.primaryemail.trim(), + primarycontact: normalisePhone(state.store.primarycontact), + address: state.store.address.trim(), + city: state.store.city.trim(), + state: state.store.state.trim(), + postcode: state.store.postcode.trim(), + tenanttype: state.store.tenanttype, + }); + await shop.refetch(); + } catch (cause) { + setSaveError(errorMessage(cause)); + return; + } finally { + setSaving(false); + } + } + + if (step === 'delivery') { + const found = validateDelivery(state.delivery); + setErrors(found); + if (!isValid(found)) return; + // Delivery is a property of the OUTLET, not the business — a merchant + // with three shops can cover different areas from each. + if (current?.locationid) { + setSaving(true); + try { + await tenantsApi.updateBranch({ + locationid: current.locationid, + tenantid, + ...(state.delivery.offersDelivery + ? { + // Metres on the wire; kilometres are what a shopkeeper thinks in. + deliveryradius: Math.round(Number(state.delivery.radiusKm) * 1000), + deliverymins: Number(state.delivery.mins), + } + : { deliveryradius: 0, deliverymins: 0 }), + }); + } catch (cause) { + setSaveError(errorMessage(cause)); + return; + } finally { + setSaving(false); + } + } + } + + if (step === 'review') { + persist({ ...completeStep(state, 'review', 'done'), finished: true }); + return; + } + + persist(completeStep(state, step, nextOf(step))); + } + + const heading = HEADINGS[step]; + const isFirst = step === 'welcome'; + const isLast = step === 'done'; + // Catalogue and inventory are the two a shop can genuinely not be ready for. + const canSkip = step === 'catalogue' || step === 'inventory'; + + return ( + + {!isFirst && !isLast ? ( + + + + {heading.title} + + {heading.blurb ? ( + + {heading.blurb} + + ) : null} + + + + ) : null} + + {step === 'welcome' ? ( + 0} + onStart={() => goTo(done > 0 ? state.step === 'welcome' ? 'store' : state.step : 'store')} + /> + ) : null} + + {step === 'store' ? ( + setState((prev) => writeOnboarding(userid, tenantid, { ...prev, store }))} + /> + ) : null} + + {step === 'catalogue' ? ( + navigate('/admin/inventory?tab=products&upload=1')} + onManual={() => navigate('/admin/inventory')} + /> + ) : null} + + {step === 'inventory' ? ( + navigate('/admin/inventory')} + onUpload={() => navigate('/admin/inventory')} + /> + ) : null} + + {step === 'delivery' ? ( + + setState((prev) => writeOnboarding(userid, tenantid, { ...prev, delivery })) + } + /> + ) : null} + + {step === 'review' ? ( + + ) : null} + + {step === 'done' ? ( + navigate('/admin/inventory')} + onInventory={() => navigate('/admin/inventory')} + onStorefront={() => navigate('/admin/inventory')} + onDashboard={() => navigate('/admin/console')} + /> + ) : null} + + {saveError ? ( + + + {saveError} + + + ) : null} + + {/* Footer navigation. Back is quiet, Continue is the one action, and Skip + sits between them so it is available without competing. */} + {!isFirst && !isLast ? ( + + + ); +} diff --git a/src/features/onboarding/steps/DeliveryStep.tsx b/src/features/onboarding/steps/DeliveryStep.tsx new file mode 100644 index 0000000..bcb0277 --- /dev/null +++ b/src/features/onboarding/steps/DeliveryStep.tsx @@ -0,0 +1,197 @@ +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { TimeInput } from '@astryxdesign/core/TimeInput'; +import { createISOTimeString, type ISOTimeString } from '@astryxdesign/core/utils'; +import { VStack } from '@astryxdesign/core/VStack'; +import { Bike, Store } from 'lucide-react'; +import type { DeliveryDraft } from '../onboardingState'; +import type { Errors } from '../validation'; + +/** + * One question, and then only the fields it makes relevant. + * + * A shop that does not deliver is asked nothing further — showing eight + * greyed-out delivery fields to a counter-only shop is how a form teaches + * somebody that most of it does not apply to them, and they stop reading the + * parts that do. + * + * The radius is the field that matters most and the least obvious: it decides + * which addresses the customer app will serve at all, so it says so rather than + * sitting there as a number. + */ +export interface DeliveryStepProps { + value: DeliveryDraft; + errors: Errors; + onChange: (next: DeliveryDraft) => void; +} + +function asTime(value: string): ISOTimeString | undefined { + return createISOTimeString(value) ?? undefined; +} + +export function DeliveryStep({ value, errors, onChange }: DeliveryStepProps) { + const set = (key: K) => (next: DeliveryDraft[K]) => + onChange({ ...value, [key]: next }); + + return ( + + + + Do you deliver to customers? + + +
+ } + title="Yes, we deliver" + body="Customers can order to their address within the area you cover." + isSelected={value.offersDelivery === true} + onSelect={() => set('offersDelivery')(true)} + /> + } + title="No, collection only" + body="Customers order and come to the shop to pick up." + isSelected={value.offersDelivery === false} + onSelect={() => set('offersDelivery')(false)} + /> +
+ + {errors['offersDelivery'] ? ( + + {errors['offersDelivery']} + + ) : null} +
+ + {/* Only when it applies. Nothing below exists for a collection-only shop, + and rendering it disabled would be showing work that is not theirs. */} + {value.offersDelivery === true ? ( + + + + + Delivery settings + + + Only the first two are required. The rest can be set later. + + + +
+ + + + + +
+ + + + When do you deliver? + +
+ set('startTime')(String(next ?? value.startTime))} + /> + set('endTime')(String(next ?? value.endTime))} + /> +
+
+
+
+ ) : null} + + {value.offersDelivery === false ? ( + + + Your shop will be listed for collection. You can turn delivery on later from your shop + settings without redoing any of this. + + + ) : null} +
+ ); +} + +function ChoiceCard({ + icon, + title, + body, + isSelected, + onSelect, +}: { + icon: React.ReactNode; + title: string; + body: string; + isSelected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} diff --git a/src/features/onboarding/steps/DoneStep.tsx b/src/features/onboarding/steps/DoneStep.tsx new file mode 100644 index 0000000..7df8679 --- /dev/null +++ b/src/features/onboarding/steps/DoneStep.tsx @@ -0,0 +1,120 @@ +import { Button } from '@astryxdesign/core/Button'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { VStack } from '@astryxdesign/core/VStack'; +import { ArrowRight, Boxes, Check, LayoutDashboard, PackageSearch } from 'lucide-react'; + +/** + * The end. + * + * One quiet animation — a tick that draws itself once — and then straight to + * what comes next. Confetti on a business tool is a celebration the user did + * not ask for, and it delays the three things they actually came here to do. + * + * The three cards are the real next actions, not a tour: each is a screen that + * exists and does something. + */ +export interface DoneStepProps { + productCount: number; + onCatalogue: () => void; + onInventory: () => void; + onStorefront: () => void; + onDashboard: () => void; +} + +export function DoneStep({ + productCount, + onCatalogue, + onInventory, + onStorefront, + onDashboard, +}: DoneStepProps) { + return ( + + + + + + + You’re all set 🎉 + + + {productCount > 0 + ? 'Your store is set up and your products are ready for customers.' + : 'Your store is set up. Add some products and they will be ready for customers.'} + + + +
+ } + title="Manage your catalogue" + body="Add products, set prices, and release them to your shops." + cta="Open catalogue" + onClick={onCatalogue} + /> + } + title="Update your stock" + body="Upload your latest counts so customers see what is really there." + cta="Update stock" + onClick={onInventory} + /> + } + title="See your shop" + body="Check what a customer sees, and which products are on sale." + cta="View products" + onClick={onStorefront} + /> +
+ + + + ); +} diff --git a/src/features/onboarding/steps/InventoryStep.tsx b/src/features/onboarding/steps/InventoryStep.tsx new file mode 100644 index 0000000..0698c91 --- /dev/null +++ b/src/features/onboarding/steps/InventoryStep.tsx @@ -0,0 +1,143 @@ +import { Button } from '@astryxdesign/core/Button'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { VStack } from '@astryxdesign/core/VStack'; +import { ArrowRight, Download, Upload } from 'lucide-react'; + +/** + * Keeping the online stock in step with the shelf. + * + * Most of these shops have no till system, so the honest answer is a + * spreadsheet — and the risk with a spreadsheet is that it reads as a technical + * chore. The flow is therefore drawn rather than described: six short stages + * from "what is on your shelf" to "what a customer sees", so a shopkeeper can + * see where their file goes before deciding to make one. + * + * Nothing here uploads. The step explains the routine and hands over to the + * screen that does it, because stock is a thing done every week and setup is a + * thing done once. + */ +export interface InventoryStepProps { + onDownloadTemplate: () => void; + onUpload: () => void; +} + +const FLOW = [ + 'What is on your shelf', + 'Written into the stock sheet', + 'Uploaded here', + 'Checked for mistakes', + 'Your stock is updated', + 'Customers see what is in stock', +] as const; + +const TIPS = [ + 'Start from the template — the columns have to match.', + 'Keep the SKU or product code exactly as it appears in your catalogue.', + 'Send the whole current count, not just what changed.', + 'Read the check results before confirming — a rejected row is a product that will not sell.', + 'Upload again whenever the shelf changes. There is no limit.', +] as const; + +export function InventoryStep({ onDownloadTemplate, onUpload }: InventoryStepProps) { + return ( + + + + + + How stock reaches your customers + + + You do not need a till system. A spreadsheet is enough. + + + +
    + {FLOW.map((stage, i) => ( +
  1. + + {i + 1} + + + {stage} + +
  2. + ))} +
+
+
+ + + + + + + Stock by spreadsheet + + + Upload your latest counts whenever they change. We check the file first and tell you + about any row we could not read. + + + + Recommended + + + + + + + JPG, PNG or WebP, up to 2 MB. Optional. + + + + take(event.target.files?.[0])} + style={{ display: 'none' }} + aria-hidden + tabIndex={-1} + /> +
+ + {/* The honest alternative: the field is a string, so a hosted image works + just as well and costs the database nothing. */} + + + {problem ? ( + + {problem} + + ) : null} +
+ ); +} diff --git a/src/features/onboarding/steps/WelcomeStep.tsx b/src/features/onboarding/steps/WelcomeStep.tsx new file mode 100644 index 0000000..2d33bb8 --- /dev/null +++ b/src/features/onboarding/steps/WelcomeStep.tsx @@ -0,0 +1,149 @@ +import { Button } from '@astryxdesign/core/Button'; +import { Card } from '@astryxdesign/core/Card'; +import { HStack } from '@astryxdesign/core/HStack'; +import { Text } from '@astryxdesign/core/Text'; +import { VStack } from '@astryxdesign/core/VStack'; +import { ArrowRight, Boxes, Rocket, Store } from 'lucide-react'; + +/** + * The first screen, and the only one whose job is not to collect anything. + * + * It exists to get somebody to begin, so it says what setup is worth and what + * it costs, and nothing else. The temptation is to explain the whole product + * here; a shopkeeper who has just signed in has no way to use any of it yet, + * and a wall of text before the first field is what makes people close the tab. + * + * The one promise made in words is the one the state layer actually keeps: + * progress is saved, so leaving is safe. + */ +export interface WelcomeStepProps { + shopName?: string; + done: number; + total: number; + isReturning: boolean; + onStart: () => void; +} + +const BENEFITS = [ + { + icon: Rocket, + title: 'Quick setup', + body: 'Five short steps. Most shops are through it in a few minutes.', + }, + { + icon: Boxes, + title: 'Manage your catalogue', + body: 'Upload a spreadsheet or add products one at a time — whichever you have.', + }, + { + icon: Store, + title: 'Ready for customers', + body: 'Finish, and your products are on sale in the app.', + }, +] as const; + +export function WelcomeStep({ shopName, done, total, isReturning, onStart }: WelcomeStepProps) { + const pct = total === 0 ? 0 : Math.round((done / total) * 100); + + return ( + + + {/* A mark rather than an illustration. A stock drawing would be the one + thing on screen not drawn from the console's own design language. */} + + + + + + {isReturning ? 'Welcome back 👋' : `Welcome${shopName ? ` to ${shopName}` : ''}! 👋`} + + + {isReturning + ? `You have completed ${done} of ${total} setup steps. Pick up where you left off.` + : "Let's get your store ready to start selling."} + + + +
+ {BENEFITS.map(({ icon: Icon, title, body }) => ( + + + + + + + {title} + + + {body} + + + + ))} +
+ + + + {done} of {total} steps completed + +
+
+
+ + + + +