Skip to content

Web: paywall UX for is_locked conversations/tasks/memories#5548

Open
omi-discord-vector[bot] wants to merge 2 commits intomainfrom
atlas/web-is-locked-paywall
Open

Web: paywall UX for is_locked conversations/tasks/memories#5548
omi-discord-vector[bot] wants to merge 2 commits intomainfrom
atlas/web-is-locked-paywall

Conversation

@omi-discord-vector
Copy link

Adds mobile-parity handling for is_locked on web.

  • Conversations: locked cards show "Upgrade to unlimited" overlay; click routes to Settings → Account with upgrade options opened
  • Tasks: locked rows show the same overlay and block edits/complete/snooze/delete (routes to upgrade)
  • Memories: locked cards show overlay and block edits/delete/visibility toggle (routes to upgrade)
  • Settings: ?upgrade=1 auto-opens the plan upgrade options

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR brings web feature-parity with the mobile app for is_locked paywall gating. A new LockedOverlay component renders a frosted-glass "Upgrade to unlimited" button over locked cards/rows, and the SettingsPage account section auto-opens the upgrade panel when navigated to with ?upgrade=1.

Key changes:

  • LockedOverlay.tsx – New shared component (absolute inset-0, z-10, pointer-events-none) with a pointer-events-auto upgrade button; aria-hidden={!onUpgrade} incorrectly silences the fallback non-interactive indicator from screen readers.
  • ConversationCard.tsx – All interaction paths (click, double-click, star) guarded for locked conversations with consistent Mixpanel tracking; "New" badge can be occluded by LockedOverlay's z-10 when a conversation is simultaneously locked and new.
  • MemoryCard.tsx / TaskRow.tsx – All mutation paths guarded and action buttons hidden for locked items; neither component fires MixpanelManager.track('Paywall Opened', ...) on redirect, unlike ConversationCard, leaving a gap in paywall analytics.
  • SettingsPage.tsxuseSearchParams used in UsageSectionContent to detect ?upgrade=1; the useEffect dependency on the derived boolean means repeat navigations to the same URL may not re-open the panel if the component doesn't remount.
  • types/conversation.tsis_locked?: boolean added to ActionItem with backwards-compat documentation.

Confidence Score: 4/5

  • PR is safe to merge — all critical interaction paths are correctly gated; issues found are analytics gaps and minor UX/accessibility concerns.
  • The paywall-gating logic is solid: locked state is checked before every mutation, the overlay is rendered correctly with proper z-index and pointer-event handling, and the settings redirect works as intended. The main concerns are (1) missing Mixpanel tracking in MemoryCard and TaskRow that would affect analytics fidelity, (2) an aria-hidden inversion in LockedOverlay that could affect accessibility, (3) a rare z-index conflict between the overlay and the "New" badge, and (4) a useEffect that may not re-trigger the upgrade panel on repeat navigations. None of these are blockers for correctness or data integrity.
  • web/app/src/components/ui/LockedOverlay.tsx (accessibility), web/app/src/components/memories/MemoryCard.tsx and web/app/src/components/tasks/TaskRow.tsx (missing Mixpanel events).

Important Files Changed

Filename Overview
web/app/src/components/ui/LockedOverlay.tsx New reusable component rendering a frosted-glass overlay with an "Upgrade to unlimited" button; pointer-events-none on the container correctly passes clicks through to parent handlers, but aria-hidden={!onUpgrade} incorrectly hides the locked indicator from screen readers in the fallback (no-handler) case.
web/app/src/components/conversations/ConversationCard.tsx Locks all interaction paths (click, double-click, star) for is_locked conversations with consistent Mixpanel tracking and upgrade routing; minor z-index conflict where the "New" badge (z-auto) can be occluded by the LockedOverlay (z-10) for conversations that are simultaneously locked and new.
web/app/src/components/memories/MemoryCard.tsx Correctly hides action buttons and guards all mutation paths (edit, delete, visibility toggle, double-click) when is_locked; missing Mixpanel 'Paywall Opened' events on every locked redirect unlike ConversationCard, leaving a gap in paywall analytics.
web/app/src/components/tasks/TaskRow.tsx Adds relative positioning to the task row and guards all action buttons (complete, edit, snooze, delete, date) for locked tasks; same missing Mixpanel tracking as MemoryCard when redirecting to the upgrade page.
web/app/src/components/settings/SettingsPage.tsx Adds ?upgrade=1 query-param detection in UsageSectionContent to auto-open the plan upgrade panel; useEffect dependency on the boolean shouldOpenUpgrade won't re-trigger if the user dismisses the panel and navigates back to the same URL without remounting the component.
web/app/src/types/conversation.ts Adds optional is_locked?: boolean to ActionItem with backwards-compat documentation comment; Memory and Conversation already had non-optional is_locked: boolean, so the new field is consistent with the existing pattern.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant Card as Card (Conv/Memory/Task)
    participant LO as LockedOverlay
    participant Router as Next.js Router
    participant SP as SettingsPage
    participant UP as UpgradePanel

    U->>Card: Interact (click / action button)
    Card->>Card: Check is_locked
    alt is_locked = true
        Card->>LO: Render LockedOverlay (absolute inset-0, z-10)
        U->>LO: Click "Upgrade to unlimited" button
        LO->>LO: e.stopPropagation()
        LO->>Router: push('/settings?section=account&upgrade=1')
        Router->>SP: Mount with section=account & upgrade=1
        SP->>SP: useSearchParams → shouldOpenUpgrade=true
        SP->>UP: useEffect → setShowUpgradeOptions(true)
        UP-->>U: Upgrade plan panel auto-opened
    else is_locked = false
        Card->>Card: Execute normal action
    end
Loading

Comments Outside Diff (4)

  1. web/app/src/components/memories/MemoryCard.tsx, line 110-115 (link)

    Missing Mixpanel paywall tracking

    MemoryCard (and TaskRow) silently redirects to the upgrade page on locked interactions but never fires a 'Paywall Opened' Mixpanel event. ConversationCard fires one for every locked interaction point (star, click, double-click). This gap means the paywall conversion funnel will be missing memory- and task-sourced touchpoints in analytics.

    For example, handleDelete should fire before redirecting:

    The same pattern applies to handleSaveEdit, handleToggleVisibility, handleTextDoubleClick, handleCardDoubleClick, and all locked checks in TaskRow.

  2. web/app/src/components/ui/LockedOverlay.tsx, line 26 (link)

    aria-hidden logic hides locked indicator from screen readers

    aria-hidden={!onUpgrade} evaluates to true when no onUpgrade handler is provided, which completely hides the locked-state indicator from assistive technologies. A user relying on a screen reader would receive no feedback that content is locked. The intent appears to be the inverse — the non-interactive fallback <div> should still be announced to convey the locked state.

    Consider using aria-label on the container and removing aria-hidden entirely, or at least defaulting to aria-hidden={false}:

  3. web/app/src/components/conversations/ConversationCard.tsx, line 261-272 (link)

    "New" badge occluded by LockedOverlay z-index

    LockedOverlay is positioned with z-10, while the "New" badge rendered after it in the DOM carries no explicit z-index (effectively z-auto / 0). In CSS stacking, an element with z-index: 10 always renders above a sibling with z-index: auto, regardless of DOM order. For a conversation that is simultaneously locked and newly created (possible when a user hits their limit mid-session), the "New" badge will be fully hidden underneath the overlay.

    Swapping the render order so isNew badge comes first, or giving it a higher z-index (e.g., z-20), would ensure both elements are visible simultaneously:

  4. web/app/src/components/settings/SettingsPage.tsx, line 835-839 (link)

    useEffect re-run won't reopen upgrade panel on repeat visits

    shouldOpenUpgrade is derived once from searchParams and stays true for the lifetime of this component mount as long as the URL contains ?upgrade=1. The useEffect runs on the first mount and sets showUpgradeOptions = true. If the user dismisses the upgrade panel, navigates away (same page, different section), and then back to ?section=account&upgrade=1, the component may not remount, meaning shouldOpenUpgrade doesn't change value and the effect does not re-run — leaving the upgrade panel closed.

    Consider also responding to showUpgradeOptions being reset to false, or removing the upgrade query param from the URL (via router.replace) after the effect fires so that a fresh navigation always triggers the open behaviour.

Last reviewed commit: 74095f9

@mdmohsin7
Copy link
Member

@greptile-apps review

@mdmohsin7
Copy link
Member

@krushnarout can you pls test this PR once and lmk if it works? thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant