Work / Chitragupt / Wiki / Decisions
2026-06-02-global-upload-plan-picker-modals
Decisioncanonicalverified 2026-06-26
DECISION.2026-06-02.GLOBAL-UPLOAD-PLAN-PICKER-MODALSGlobal 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
UploadModalProvidermounted inapps/website/src/app/(app)/layout.tsx. HookuseUploadModal()returns{ openUpload, closeUpload, isOpen }. Modal wraps the existingDocumentUploadercomponent — same Firebase Storage path, same MIME/size guards, same drag-drop affordance./inboxpage 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").PlanPickerProvidermounted alongside. HookusePlanPicker()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 (₹250Self→AI Self,₹300AI Self→Pro Family,₹550Self→Pro Family). Razorpay runs inline via a new shared hookuseUnlockCheckout()(extracted fromPlanForm.tsx) — samerequestUnlock→verifyUnlockPaymentflow, 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,AskSidebarupsell,ExpenseNextMoveAsideAI advisor cards,PortfolioNextMovesAsideCardAI 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/planrewritten to useuseUnlockCheckout()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;
/upgradepage +PaywallModaldeleted"
Every project of mine is written down like this.
Read the résumé