Seller Onboarding Live
The 4-screen welcome tour + 6-step guided wizard that takes a non-technical Gadag shopkeeper from "just registered" to "store fully configured" in under 5 minutes.
Shipped 2026-05-03. Replaces the v1 stub at mobile-seller/(onboarding)/store-type.tsx which sent sellers to ka26.shop manually.
1. Why this matters
Onboarding friction was the #1 blocker on seller acquisition. A non-technical shopkeeper opening the seller app for the first time would see "Set up your store on ka26.shop" and close the app. The seller-mobile-app spec §4.1 promised a guided wizard — this delivers it.
The brief: any rural seller, regardless of technical ability, must be able to set up their store quickly without help. If they can't complete it in 5 minutes solo, it's not done.
2. The flow
Login (first time)
│
▼
┌─────────────────────────────────┐
│ TOUR (4 swipeable cards) │ ~30 sec
│ · Welcome │
│ · Orders │
│ · Bids │
│ · Earnings │
└────────────────┬────────────────┘
│ PATCH /api/seller/onboarding
│ → flips Seller.tourCompletedAt
▼
┌─────────────────────────────────┐
│ WIZARD (6 steps) │ ~3-5 min
│ 1. Shop photos (1-5) │
│ 2. Shop name │
│ 3. Working hours │
│ 4. Service (pickup/del/both) │
│ 5. Delivery details (if del) │
│ 6. Confirmation │
└────────────────┬────────────────┘
│ POST /api/seller/onboarding (single shot)
│ → creates/updates Store + StoreImage rows
│ → flips Seller.onboardingCompletedAt
▼
┌─────────────────────────────────┐
│ DONE │ celebration
│ · animated check │
│ · "what happens next" cards │
└────────────────┬────────────────┘
│
▼
/(onboarding)/pending
(waiting for admin to verify)
3. Routing matrix (in mobile-seller/app/index.tsx)
The root entry runs this matrix on every cold start:
| Condition | Route |
|---|---|
| no seller (not logged in) | /(auth)/login |
tourCompletedAt is null | /(onboarding)/tour |
onboardingCompletedAt is null | /(onboarding)/wizard |
status === "pending" | /(onboarding)/pending |
status === "active" | /(tabs)/orders |
status === "disabled" | /(auth)/login |
First match wins. The matrix is the single source of truth — every screen relies on it being correct.
4. The wizard's UX commitments
- One question per screen. No "fill out this form."
- Large 56pt buttons — every CTA hits the lower-third thumb zone.
- Minimal text. Labels, not paragraphs.
- Progress shown — "Step 3 / 6" + thin bar fill.
- Edit any step from confirmation — tap any line in step 6 to jump back.
- No vertical scrolling within a step (except step 1 photo grid + step 6 summary).
- Service-branch optimization — step 5 (delivery details) is automatically skipped if service=="pickup". Saves 1 screen for ~30% of sellers.
5. Validation rules (server-side, in POST /api/seller/onboarding)
| Field | Rule | Error message (verbatim) |
|---|---|---|
storeName | trimmed length ≥ 2, ≤ 80 | "Please enter your shop name" |
imageUrls | array, ≥ 1 item, capped at 5 | "Please upload at least one shop photo" |
openingTime | regex `/^([01]\d | 2[0-3]):[0-5]\d$/` |
closingTime | same regex | "Closing time must look like "22:00"" |
openingTime !== closingTime | distinct | "Opening and closing time can't be the same" |
service | one of pickup/delivery/both | "Please pick pickup, delivery, or both" |
deliveryRadiusKm | required if delivery, > 0, ≤ 50 | "Please pick a delivery distance" / "Delivery distance can be at most 50 km" |
sameDayOrderCutoff | optional HH:MM if delivery | "Same-day cutoff must look like "17:00"" |
Mobile UI also gates each step locally — the Next button stays disabled until the current step's data is valid. Server validates again as defence-in-depth.
6. Idempotency
POST /api/seller/onboarding is idempotent at the seller level:
- First call: creates new Store + StoreImage rows.
- Second call (same seller): updates the existing store, wipes + re-writes images to match the new selection (not appended).
storeIdreturned is stable across calls.
This protects against:
- Double-tap on the final "Confirm" button on a slow connection.
- Retry-on-network-flake on weak rural data — both common at our user base.
- The "edit your store" use case once verification is later added (same endpoint).
7. Data model
Schema additions (additive, deployed 2026-05-03)
Seller
tourCompletedAt DateTime?
onboardingCompletedAt DateTime?
Store
sameDayOrderCutoff String? // "HH:MM" — orders before this time today get same-day delivery
Existing fields used by the wizard:
Store.name,Store.imageUrl(first),Store.openingTime,Store.closingTimeStore.supportsDelivery,Store.supportsPickup,Store.deliveryAvailableStore.serviceAreaMode(set to"radius"when delivery enabled),Store.deliveryRadiusKmStore.status(starts as"coming_soon", admin verification flips to"live")StoreImage(1-5 rows per store with display order)
8. API reference
GET /api/seller/onboarding
Returns the seller's onboarding state — used by the mobile app's routing matrix on cold start.
{
"tourCompleted": false,
"onboardingCompleted": false,
"hasStore": false
}
PATCH /api/seller/onboarding
Marks the welcome tour as seen. Body is empty. Idempotent.
{ "ok": true }
POST /api/seller/onboarding
The big one. Body:
{
"storeName": "Mahalaxmi Furniture Mart",
"imageUrls": ["https://...", "https://..."],
"openingTime": "09:00",
"closingTime": "21:00",
"service": "both",
"deliveryRadiusKm": 15,
"sameDayOrderCutoff": "16:00",
"storeType": "general"
}
Response:
{
"ok": true,
"storeId": 42,
"message": "Your shop is set up. A KA26 team member will visit within 48-72 hours to verify, then your shop goes live."
}
Auth: requires authenticated seller via cookie or Bearer. Status doesn't have to be "active" — onboarding runs while seller is still "pending".
Rate-limited: 5 attempts per seller per 5 min via PRODUCT_CREATE bucket.
9. i18n
61 new keys split across 3 namespaces:
tour.*— 12 keys (welcome cards + Skip / Next / Got it)wizard.*— 41 keys (6 step questions/hints + chip labels + error alerts)done.*— 8 keys (success screen + 3 next-step cards)
| Locale | Coverage |
|---|---|
en.json | Native — 61 keys |
kn.json | Proper Kannada translations — 61 keys (regression test pins Kannada Unicode block presence on headline keys) |
hi.json, ta.json, te.json, ml.json, mr.json | English fallback (LanguageContext fallback at render time — non-blocker for i18n test parity) |
10. Tests
tests/seller-onboarding.test.ts — 33 regression-pinning assertions:
- Schema fields exist
- All 4 HTTP method handlers exported
- Auth + rate limit applied
- Each validation rule fires its specific error message
- Idempotency (find existing → update; otherwise create)
- Image wipe + re-write (not append) on update
onboardingCompletedAtflipped on success- All 6 wizard step components exist + canProceed gating
- Pickup-only short-circuit skips delivery details
- Routing matrix preserves tour-before-wizard-before-pending order
- All 7 locales contain every new namespace key
- kn.json has actual Kannada (not English fallback) on headline keys
Full project after this change: 2,783 / 2,783 passing.
11. Live verification (2026-05-03)
/tmp/onboarding-e2e.sh — registers a fresh "Mahalaxmi Furniture Mart" seller against prod, walks through every endpoint:
- POST /api/auth/register → seller ID
- Mint JWT directly (skip OTP)
- GET /api/seller/onboarding (all flags false)
- PATCH → tourCompleted true
- GET → confirms tour flag set
- POST without images → 400 "Please upload at least one shop photo"
- POST with malformed time → 400 'Opening time must look like "08:00"'
- POST with delivery but no radius → 400 "Please pick a delivery distance"
- POST with full valid payload → 200 + storeId
- GET → onboardingCompleted true
- POST same payload again → same storeId returned (idempotent)
Every assertion passes against prod.
12. Known follow-ups
- Stock-photo library — when a seller has no shop photos, fall back to a category-curated stock image with a "✱ Stock photo" badge and prompt for real photos via WhatsApp later.
- Photo-to-catalog (agentic AI) — the natural next step. Once seller is in via this wizard, photographing a handwritten Kannada product list builds the initial catalog automatically. See agentic-AI strategy from 2026-05-02.
- Edit-store screen in the main app — currently sellers can re-run the wizard via the same endpoint, but a dedicated edit screen in
/(tabs)/storeswould be cleaner. - Voice-first wizard — given KA26's existing voice infrastructure, the Step 2 (shop name) could accept voice input. Tier-3 sellers spell their shop names oddly; voice + Sarvam STT would feel magical.
13. Web parity (added the same day)
The same flow runs on the web seller app — /seller/onboarding. Same backend endpoint, same UX commitments, same step contract, same routing matrix:
no seller → /seller/login
no tourCompletedAt → /seller/onboarding (tour phase)
no onboardingCompletedAt → /seller/onboarding (wizard phase)
status pending → /seller/pending
status active → /seller/stores
The web wizard uses Tailwind + Lucide icons (instead of React Native + Feather). The legacy 5-step doc-upload flow at /seller/onboarding was replaced; the file now contains the matching 6-step + tour + done flow. Reference for the legacy version is preserved in git history (commit predating 2026-05-03).
/seller/pending (NEW)
Shown to web sellers who finished the wizard but await admin approval. Mirrors mobile-seller/(onboarding)/pending. Includes:
- Status timeline (account created → shop set up → in-person verification → live)
- Support contact (call + WhatsApp)
- Logout
Layout routing fix
Previously src/app/seller/layout.tsx bounced ALL non-active sellers to /seller/onboarding — caused an infinite loop after a seller finished the wizard. Now routes "pending + onboardingCompletedAt set" to /seller/pending.
14. Admin verification queue fix (same-day)
User-reported gap: completed-onboarding sellers weren't appearing in /admin/verification.
Root cause: the queue API filtered on submittedForReviewAt: { not: null } — that field is set by the legacy explicit-submit flow, NOT by the new wizard. The wizard sets onboardingCompletedAt instead. So every seller who completed the new flow was invisible to admins.
Fix: the queue + count + ordering all now accept BOTH timestamps via OR clause. The verification page reads onboardingCompletedAt as a fallback display timestamp. No nav changes needed — the side-nav badge picks it up automatically.
// Before: where: { submittedForReviewAt: { not: null } }
// After: where: { OR: [
// { submittedForReviewAt: { not: null } },
// { onboardingCompletedAt: { not: null } },
// ] }
15. Related
- Bidding — the bid lifecycle the tour explains in screen 3
- Admin Panel — where pending sellers from this flow appear for verification
- Account Deletion — the parallel destructive flow on the consumer app
- Why monolith — why the seller mobile + backend live together
- CHANGELOG entry — full context