Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-06-02-global-upload-plan-picker-modals

Decisioncanonicalverified 2026-06-26

DECISION.2026-06-02.GLOBAL-UPLOAD-PLAN-PICKER-MODALS

Global Upload modal + global Plan-picker modal; /upgrade page + PaywallModal deleted

Decision

Mount a global Upload modal (UploadModalProvider) and a global Plan-picker modal (PlanPickerProvider) in the website app layout. Convert every "upload" and "upgrade" CTA to open these modals in-place. Delete /upgrade, PaywallModal, and components/upgrade/*.

Why

Two related UX bugs surfaced together. (a) Every "drop a doc" CTA across Dashboard / Tax / Expense / Portfolio / Ask routed users to /inbox before they could upload — a forced context switch. (b) Every "Unlock parsing" / "Upgrade" CTA routed users to /upgrade (Pro Family only) or to /onboarding/plan (Self + Pro Family only, AI Self missing entirely). Users could not purchase AI Self at all post-onboarding, and could only buy Self by going back to onboarding. Both flows now resolve to in-place modals.

Impact

  • UploadModalProvider mounted in apps/website/src/app/(app)/layout.tsx. Hook useUploadModal() returns { openUpload, closeUpload, isOpen }. Modal wraps the existing DocumentUploader component — same Firebase Storage path, same MIME/size guards, same drag-drop affordance. /inbox page keeps its inline uploader; CTAs elsewhere now open the modal: dashboard "Upload Form 16", FirstUploadCta, PillarEmptyView (Expense/Portfolio empty states), AskEmptyState, DocumentsFedIn (Tax review "upload more").
  • PlanPickerProvider mounted alongside. Hook usePlanPicker() returns { openPlanPicker, closePlanPicker, isOpen }. Modal shows all four tiers (Free / Self ₹249 / AI Self ₹499 / Pro Family ₹799), marks the user's current plan, and renders eligible upgrade prices using the canonical topup table (₹250 Self→AI Self, ₹300 AI Self→Pro Family, ₹550 Self→Pro Family). Razorpay runs inline via a new shared hook useUnlockCheckout() (extracted from PlanForm.tsx) — same requestUnlock → verifyUnlockPayment flow, same sessionStorage-backed verify-retry. On success the modal auto-closes (no route change); the subscription store updates via the existing Firestore snapshot.
  • All "Unlock"/"Upgrade" CTAs converted to openPlanPicker(…): DocViewOnlyView (inbox parsing), DocOwnerMismatchView (Pro Family transfer), PillarLockedView (Expense/Portfolio locked), PillarFamilyLockedView, TaxLockedView, TaxFamilyLockedView, AskLockedState, AskQuotaExhaustedState, AskSidebar upsell, ExpenseNextMoveAside AI advisor cards, PortfolioNextMovesAsideCard AI advisor card, InboxRightRail, FamilyOverviewHeader, CurrentPlanCard ("See all plans"), UpgradeCard ("Upgrade for ₹…").
  • Deletions (no-legacy): apps/website/src/app/(app)/upgrade/ (page + success page), apps/website/src/components/upgrade/ (7 components), apps/website/src/components/payment/PaywallModal.tsx. All callers swept in the same change.
  • /onboarding/plan rewritten to use useUnlockCheckout() and to include the missing AI Self radio card. AI Self / Pro Family / Self all selectable in onboarding (previously only Self + Pro Family).
  • Onboarding routing: after payment or skip, both paths route directly to /dashboard.
  • Marketing pricing page (PlatformPlans) repoints to /onboarding/plan?plan=pro_family (marketing routes don't have the modal provider mounted).
  • Affects tier-free, tier-self, tier-ai-self, tier-pro-family, pricing-upgrade-topups, pricing, upload-only, pillar-inbox, pillar-tax, pillar-expense, pillar-portfolio, and copy-strings.

Status

Active.

Sources

  • .context/wiki/decisions/* § "2026-06-02 — Global Upload modal + global Plan-picker modal; /upgrade page + PaywallModal deleted"

Every project of mine is written down like this.

Read the résumé