Skip to main content

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:

ConditionRoute
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)

FieldRuleError message (verbatim)
storeNametrimmed length ≥ 2, ≤ 80"Please enter your shop name"
imageUrlsarray, ≥ 1 item, capped at 5"Please upload at least one shop photo"
openingTimeregex `/^([01]\d2[0-3]):[0-5]\d$/`
closingTimesame regex"Closing time must look like "22:00""
openingTime !== closingTimedistinct"Opening and closing time can't be the same"
serviceone of pickup/delivery/both"Please pick pickup, delivery, or both"
deliveryRadiusKmrequired if delivery, > 0, ≤ 50"Please pick a delivery distance" / "Delivery distance can be at most 50 km"
sameDayOrderCutoffoptional 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).
  • storeId returned 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.closingTime
  • Store.supportsDelivery, Store.supportsPickup, Store.deliveryAvailable
  • Store.serviceAreaMode (set to "radius" when delivery enabled), Store.deliveryRadiusKm
  • Store.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)
LocaleCoverage
en.jsonNative — 61 keys
kn.jsonProper Kannada translations — 61 keys (regression test pins Kannada Unicode block presence on headline keys)
hi.json, ta.json, te.json, ml.json, mr.jsonEnglish fallback (LanguageContext fallback at render time — non-blocker for i18n test parity)

10. Tests

tests/seller-onboarding.test.ts33 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
  • onboardingCompletedAt flipped 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:

  1. POST /api/auth/register → seller ID
  2. Mint JWT directly (skip OTP)
  3. GET /api/seller/onboarding (all flags false)
  4. PATCH → tourCompleted true
  5. GET → confirms tour flag set
  6. POST without images → 400 "Please upload at least one shop photo"
  7. POST with malformed time → 400 'Opening time must look like "08:00"'
  8. POST with delivery but no radius → 400 "Please pick a delivery distance"
  9. POST with full valid payload → 200 + storeId
  10. GET → onboardingCompleted true
  11. 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)/stores would 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 } },
// ] }
  • 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