Skip to content

[PULSE-35] Billing tab: cancel and change-plan UI and copy#19

Merged
uz1mani merged 8 commits intomainfrom
staging
Feb 9, 2026
Merged

[PULSE-35] Billing tab: cancel and change-plan UI and copy#19
uz1mani merged 8 commits intomainfrom
staging

Conversation

@uz1mani
Copy link
Copy Markdown
Member

@uz1mani uz1mani commented Feb 9, 2026

Work Item

PULSE-35

Summary

  • Billing tab redesign: trial banner, plan/usage card, in-app Change plan and Cancel subscription, clear copy for access end and data retention.
  • API client for cancel and change-plan; shared plan/tier definitions.

Changes

  • Billing tab (Org Settings): Trial banner with end date and “charged unless you cancel” copy; cancel-at-period-end notice when set; plan card with name, status, “Billed monthly/yearly”, “Change plan” button; 4-column usage (Sites, Pageviews, Trial ends/Renews, Limit); “Payment method & invoices” link; “Cancel subscription” bordered button (neutral, red on hover).
  • Cancel flow: Modal with “at period end” vs “immediately”, copy: “You keep access until [date]” and “Your data is retained for 30 days after access ends.”
  • Change plan modal: Tier dropdown and monthly/yearly toggle; “Start subscription” or “Update plan” depending on existing subscription; uses checkout for new, changePlan API for existing.
  • API client: cancelSubscription, changePlan; SubscriptionDetails includes cancel_at_period_end.
  • Shared: lib/plans.tsTRAFFIC_TIERS, PLAN_ID_SOLO, tier index/limit helpers for pricing and billing.

Test Plan

  • Trial user sees trial banner and correct end date; copy about auto-charge.
  • Cancel at period end and immediately both work; modal copy matches behavior.
  • Change plan: new subscription → checkout; existing → in-app update and refresh.
  • “Payment method & invoices” opens Stripe portal when applicable.
  • Dark/light theme and responsive layout look correct.

uz1mani and others added 4 commits February 9, 2026 10:25
- Add prominent trial banner at top with end date and auto-charge notice
- Separate cancel-at-period-end warning into its own banner
- Simplify plan header: name + badge + billing interval left, Change plan right
- Reduce stats grid from 5 cramped columns to 4 clean columns
- Remove redundant "Pageview Limit" stat (duplicated "Pageviews")
- Replace bulky cancel section with inline text links below the card
- Make "Payment method & invoices" a clear link with icon
- Compact invoice rows: remove avatar icons, inline date with amount
- Tighter spacing throughout for cohesive feel

Co-authored-by: Cursor <cursoragent@cursor.com>
Ciphera values clarity — if a user wants to cancel, the option should
be honest and easy to find, not hidden. Neutral bordered pill that
highlights red on hover communicates this respectfully.

Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR redesigns the Organization Settings → Billing tab UI and adds in-app subscription actions.

  • UI: Adds trial and cancel-at-period-end banners, a combined plan/usage card, a “Payment method & invoices” action, and new modals for canceling and changing plans.
  • Billing client: Extends SubscriptionDetails with cancel_at_period_end and introduces cancelSubscription and changePlan API helpers, plus reuse of checkout/portal flows.
  • Shared plan config: Adds lib/plans.ts for Solo plan traffic tiers and helper functions used by the change-plan modal.

Key integration point is components/settings/OrganizationSettings.tsx, which now orchestrates calls to the billing API helpers and redirects to Stripe when needed.

Confidence Score: 3/5

  • This PR is mergeable after fixing a couple of user-facing billing logic/UX bugs in the Billing tab.
  • Most changes are UI/copy and thin API wrappers, but there are two definite behavioral issues: (1) trialing subs without a payment method are treated as “no subscription” and routed to checkout, and (2) invoice loading/empty states are unreachable because rendering is gated by invoices.length > 0. These should be corrected before merge.
  • components/settings/OrganizationSettings.tsx

Important Files Changed

Filename Overview
components/settings/OrganizationSettings.tsx Redesigned Billing tab with trial/cancel/change-plan UI and new modals; found logic issues where trialing w/o payment method is treated as no subscription and invoices UI is hidden behind invoices.length > 0 so loading/empty states never show.
lib/api/billing.ts Added cancel_at_period_end to SubscriptionDetails and new cancelSubscription/changePlan API helpers; appears consistent with existing billingFetch usage.
lib/plans.ts Introduced shared Solo plan ID and traffic tier helpers; simple mapping with sane defaults.

Sequence Diagram

sequenceDiagram
  autonumber
  actor User
  participant UI as OrganizationSettings(Billing tab)
  participant BillingAPI as lib/api/billing.ts
  participant Backend as /api/billing/*
  participant Stripe as Stripe (portal/checkout)

  User->>UI: Open Org Settings → Billing tab
  UI->>BillingAPI: getSubscription()
  BillingAPI->>Backend: GET /api/billing/subscription
  Backend-->>BillingAPI: SubscriptionDetails (incl cancel_at_period_end)
  BillingAPI-->>UI: subscription
  UI->>BillingAPI: getInvoices()
  BillingAPI->>Backend: GET /api/billing/invoices
  Backend-->>BillingAPI: Invoice[]
  BillingAPI-->>UI: invoices

  alt User clicks “Payment method & invoices”
    UI->>BillingAPI: createPortalSession()
    BillingAPI->>Backend: POST /api/billing/portal
    Backend-->>BillingAPI: {url}
    BillingAPI-->>UI: {url}
    UI->>Stripe: Redirect to Stripe portal URL
  end

  alt User opens Change plan modal
    User->>UI: Click “Change plan”
    UI->>UI: Select tier + interval
    alt Existing subscription (hasActiveSubscription)
      UI->>BillingAPI: changePlan({plan_id, interval, limit})
      BillingAPI->>Backend: POST /api/billing/change-plan
      Backend-->>BillingAPI: {ok}
      BillingAPI-->>UI: {ok}
      UI->>BillingAPI: getSubscription()
    else New subscription
      UI->>BillingAPI: createCheckoutSession({plan_id, interval, limit})
      BillingAPI->>Backend: POST /api/billing/checkout
      Backend-->>BillingAPI: {url}
      BillingAPI-->>UI: {url}
      UI->>Stripe: Redirect to Stripe checkout URL
    end
  end

  alt User cancels subscription
    User->>UI: Click “Cancel subscription”
    alt Cancel at period end
      UI->>BillingAPI: cancelSubscription({at_period_end:true})
      BillingAPI->>Backend: POST /api/billing/cancel
      Backend-->>BillingAPI: {ok, at_period_end:true}
      BillingAPI-->>UI: response
      UI->>BillingAPI: getSubscription()
    else Cancel immediately
      UI->>BillingAPI: cancelSubscription({at_period_end:false})
      BillingAPI->>Backend: POST /api/billing/cancel
      Backend-->>BillingAPI: {ok, at_period_end:false}
      BillingAPI-->>UI: response
      UI->>BillingAPI: getSubscription()
    end
  end
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR redesigns the Organization Settings → Billing tab UI and adds in-app flows to cancel a subscription (at period end vs immediately) and change plan/tier (monthly vs yearly), backed by new billing client endpoints (cancelSubscription, changePlan, and createCheckoutSession) plus shared tier definitions in lib/plans.ts.

The main integration point is components/settings/OrganizationSettings.tsx, which fetches subscription/invoice data and drives Stripe portal/checkout redirects or in-app API calls depending on subscription status. The API client changes are small and consistent with existing billingFetch usage, and lib/plans.ts centralizes tier/limit mapping for the change-plan modal.

Issues to fix before merge are localized to the billing UI: the invoices list currently contains invalid JSX around invoices.map(...) and there is a loading-state edge case in loadMembers that can leave the Members section stuck loading if org_id is initially null and later hydrates.

Confidence Score: 3/5

  • This PR is close to mergeable but has a definite UI compilation/render bug and a loading-state logic edge case that should be fixed first.
  • Most changes are UI and thin API wrappers, but OrganizationSettings.tsx currently has invalid JSX in the invoices branch (invoices.map(...) not wrapped in {}) and a loadMembers early-return that can prevent isLoadingMembers from ever being cleared when org context hydrates after first render.
  • components/settings/OrganizationSettings.tsx

Important Files Changed

Filename Overview
components/settings/OrganizationSettings.tsx Adds cancel/change-plan UI and modals; introduces a JSX render bug in the invoices list and a loading-state bug that can leave the members section stuck loading when org_id hydrates late.
lib/api/billing.ts Extends billing client with cancelSubscription and changePlan endpoints and adds cancel_at_period_end to SubscriptionDetails; changes are straightforward.
lib/plans.ts Introduces shared Solo plan traffic tiers and helpers for tier<->limit mapping; simple constants/utilities.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant OS as OrganizationSettings (Billing tab)
  participant BA as lib/api/billing.ts
  participant API as Backend /api/billing
  participant Stripe as Stripe

  U->>OS: Open Billing tab
  OS->>BA: getSubscription()
  BA->>API: GET /subscription
  API-->>BA: SubscriptionDetails
  BA-->>OS: SubscriptionDetails
  OS->>BA: getInvoices()
  BA->>API: GET /invoices
  API-->>BA: Invoice[]
  BA-->>OS: Invoice[]

  U->>OS: Click "Payment method & invoices"
  OS->>BA: createPortalSession()
  BA->>API: POST /portal
  API-->>BA: {url}
  BA-->>OS: {url}
  OS->>Stripe: Redirect to Stripe portal

  U->>OS: Click "Cancel subscription"
  OS->>BA: cancelSubscription({at_period_end})
  BA->>API: POST /cancel
  API-->>BA: {ok, at_period_end}
  BA-->>OS: {ok, at_period_end}
  OS->>BA: getSubscription() (refresh)

  U->>OS: Click "Change plan" and submit
  alt Existing subscription (active/trialing)
    OS->>BA: changePlan({plan_id, interval, limit})
    BA->>API: POST /change-plan
    API-->>BA: {ok}
    BA-->>OS: {ok}
    OS->>BA: getSubscription() (refresh)
  else No active subscription
    OS->>BA: createCheckoutSession({plan_id, interval, limit})
    BA->>API: POST /checkout
    API-->>BA: {url}
    BA-->>OS: {url}
    OS->>Stripe: Redirect to Stripe Checkout
  end
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Additional Comments (1)

components/settings/OrganizationSettings.tsx
Loading state never clears

loadMembers returns early when currentOrgId is falsy, but isLoadingMembers only gets set to false in the finally of the try/catch. If currentOrgId is temporarily null (e.g., auth context hydrates after first render), isLoadingMembers will stay stuck true because the effect’s else branch only runs when currentOrgId changes to falsy; it won’t run again when it becomes truthy. Consider ensuring the early-return path also clears the loading flag.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 62:67

Comment:
**Loading state never clears**

`loadMembers` returns early when `currentOrgId` is falsy, but `isLoadingMembers` only gets set to `false` in the `finally` of the try/catch. If `currentOrgId` is temporarily null (e.g., auth context hydrates after first render), `isLoadingMembers` will stay stuck `true` because the effect’s `else` branch only runs when `currentOrgId` changes to falsy; it won’t run again when it becomes truthy. Consider ensuring the early-return path also clears the loading flag.


How can I resolve this? If you propose a fix, please make it concise.

@uz1mani uz1mani self-assigned this Feb 9, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

Billing UI redesign adds trial banner, cancel-at-period-end notice, modernized plan/usage card, and in-app Change plan and Cancel subscription flows. Copy clearly communicates trial auto-charge, access end dates, and 30-day data retention.

Key changes:

  • Trial banner shows end date and auto-charge warning
  • Cancel-at-period-end banner with "Use Change plan to resubscribe" copy
  • Plan card with tier, status badge, billing interval, and usage stats (Sites, Pageviews, Trial ends/Renews, Limit)
  • Change plan modal with tier dropdown and monthly/yearly toggle; routes trialing users correctly through changePlan API (previous subscription gating issue fixed)
  • Cancel modal offers "at period end" or "immediately" with 30-day data retention notice
  • Invoice section now always renders (loading/empty states previously hidden)
  • New API functions: cancelSubscription, changePlan, createCheckoutSession
  • Shared plan constants in lib/plans.ts for tier definitions and limit helpers

Minor issues found:

  • Modal loading UX could be more consistent (only first button shows spinner)
  • pageview_limit fallback logic treats 0 as falsy
  • Currency formatting hardcoded to 'en-US' locale

Confidence Score: 4/5

  • Safe to merge with minor UX improvements recommended
  • Previous critical subscription gating and invoices rendering bugs have been fixed. The changes are well-structured with proper error handling and type safety. Three minor issues found (loading state UX, pageview_limit edge case, locale hardcoding) are non-blocking style/edge-case improvements that don't affect core functionality.
  • No files require special attention - all previous critical issues have been resolved

Important Files Changed

Filename Overview
lib/plans.ts New file defining traffic tiers and plan constants. Clean, well-documented utility functions with proper fallbacks.
lib/api/billing.ts Added cancel and change-plan API functions. Type-safe with proper JSDoc, follows existing patterns.
components/settings/OrganizationSettings.tsx Redesigned billing UI with cancel/change-plan modals. Previous issues addressed, but modal state could be simplified.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as OrganizationSettings
    participant API as Billing API
    participant Stripe

    Note over User,Stripe: Change Plan Flow
    User->>UI: Click "Change plan"
    UI->>UI: openChangePlanModal()<br/>(load current tier)
    UI->>User: Show modal with tier dropdown
    User->>UI: Select tier & interval
    User->>UI: Click "Update plan"/"Start subscription"
    alt Has Active Subscription (trial/active)
        UI->>API: changePlan({plan_id, interval, limit})
        API->>Stripe: Update subscription
        Stripe-->>API: Updated subscription
        API-->>UI: {ok: true}
        UI->>UI: loadSubscription()
        UI->>User: Toast: "Plan updated"
    else No Active Subscription
        UI->>API: createCheckoutSession({plan_id, interval, limit})
        API->>Stripe: Create checkout session
        Stripe-->>API: Session URL
        API-->>UI: {url}
        UI->>User: Redirect to Stripe checkout
    end

    Note over User,Stripe: Cancel Subscription Flow
    User->>UI: Click "Cancel subscription"
    UI->>User: Show cancel modal
    User->>UI: Choose "at period end" or "immediately"
    UI->>API: cancelSubscription({at_period_end})
    API->>Stripe: Cancel subscription
    Stripe-->>API: Canceled subscription
    API-->>UI: {ok: true, at_period_end}
    UI->>UI: loadSubscription()
    UI->>User: Show cancel notice banner
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR redesigns the billing tab with trial banners, cancel-at-period-end notices, and in-app change-plan and cancel-subscription flows. It adds API client functions for cancelSubscription and changePlan, and introduces a shared lib/plans.ts module for traffic tier definitions.

Key Changes:

  • Billing UI now displays trial status banner, cancel-at-period-end notice, plan card with usage stats, and clear action buttons
  • Cancel flow offers "at period end" vs "immediately" options with data retention copy
  • Change plan modal routes users through Stripe checkout for new subscriptions or in-app changePlan API for existing ones
  • New API client functions: cancelSubscription, changePlan, createCheckoutSession
  • Shared plan definitions in lib/plans.ts with TRAFFIC_TIERS and helper functions

Critical Issue:

  • JSX syntax error on line 874 in the invoices rendering will prevent compilation - the ternary third branch has mismatched braces

Notes:

  • Developer has already addressed several issues from previous review rounds (subscription gating, loading states, tier selection)
  • The logic correctly distinguishes between active/trialing subscriptions and routes to appropriate flows
  • API client functions follow existing patterns with proper error handling

Confidence Score: 2/5

  • This PR has a compilation-blocking JSX syntax error that must be fixed before merge
  • The JSX syntax error on line 874 in OrganizationSettings.tsx (mismatched braces in the invoices ternary) will cause compilation to fail. While the business logic appears sound and the developer has addressed previous issues, this syntax error is critical and prevents the code from running.
  • components/settings/OrganizationSettings.tsx has a JSX syntax error on line 874 that must be corrected

Important Files Changed

Filename Overview
components/settings/OrganizationSettings.tsx Billing tab redesign with cancel/change-plan modals; JSX syntax error in invoices rendering will prevent compilation
lib/api/billing.ts Added cancelSubscription, changePlan, and createCheckoutSession API client functions with proper TypeScript types
lib/plans.ts New shared plan definitions file with traffic tiers and helper functions for tier/limit conversions

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as OrganizationSettings
    participant API as Billing API
    participant Stripe
    
    Note over User,Stripe: Change Plan Flow
    User->>UI: Click "Change plan" button
    UI->>UI: openChangePlanModal()
    UI->>UI: Load current tier from subscription
    UI->>User: Show modal with tier selector
    User->>UI: Select tier & interval
    User->>UI: Click "Update plan" / "Start subscription"
    
    alt Has Active Subscription
        UI->>API: changePlan(plan_id, interval, limit)
        API->>Stripe: Update subscription with proration
        Stripe-->>API: Success
        API-->>UI: {ok: true}
        UI->>UI: loadSubscription()
        UI->>User: Toast: "Plan updated"
    else No Active Subscription
        UI->>API: createCheckoutSession(plan_id, interval, limit)
        API->>Stripe: Create checkout session
        Stripe-->>API: {url}
        API-->>UI: {url}
        UI->>User: Redirect to Stripe checkout
    end
    
    Note over User,Stripe: Cancel Subscription Flow
    User->>UI: Click "Cancel subscription"
    UI->>User: Show cancel modal with options
    
    alt Cancel at Period End
        User->>UI: Click "Cancel at period end"
        UI->>API: cancelSubscription({at_period_end: true})
        API->>Stripe: Set cancel_at_period_end
        Stripe-->>API: Success
        API-->>UI: {ok: true, at_period_end: true}
        UI->>UI: loadSubscription()
        UI->>User: Toast: "Subscription will cancel at end of billing period"
        UI->>User: Show cancellation notice banner
    else Cancel Immediately
        User->>UI: Click "Cancel immediately"
        UI->>API: cancelSubscription({at_period_end: false})
        API->>Stripe: Cancel subscription now
        Stripe-->>API: Success
        API-->>UI: {ok: true, at_period_end: false}
        UI->>UI: loadSubscription()
        UI->>User: Toast: "Subscription canceled"
    end
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR implements a comprehensive billing UI redesign with in-app subscription management. The changes include:

  • Trial and cancellation banners: Clear notices for trial users and subscriptions set to cancel, with access-end dates and 30-day data retention copy
  • Redesigned plan card: Displays plan name, status badges, billing interval, with a "Change plan" button (disabled when cancellation is pending)
  • Four-column usage display: Sites, Pageviews, Trial ends/Renews/Access until, and Limit
  • In-app cancel flow: Modal offering "Cancel at period end" (keeps access until renewal date) or "Cancel immediately" options, with proper loading states for each action
  • In-app change plan flow: Modal with tier dropdown and monthly/yearly toggle; routes trialing/active subscriptions through changePlan API, new subscriptions through checkout
  • API client additions: cancelSubscription and changePlan endpoints with proper TypeScript interfaces
  • Shared plan utilities: New lib/plans.ts with TRAFFIC_TIERS constants and tier index/limit conversion helpers

The implementation correctly handles the subscription status logic after addressing previous threading feedback. All previous issues around subscription gating, loading states, and invoice rendering have been resolved.

Confidence Score: 4/5

  • This PR is safe to merge with only minor suggestions for improvement
  • The implementation correctly handles subscription states and user flows after addressing all critical issues from previous review threads. The API client is well-typed, shared utilities are clean, and UI logic properly gates actions. No blocking issues remain.
  • No files require special attention

Important Files Changed

Filename Overview
components/settings/OrganizationSettings.tsx Major UI redesign for billing tab with trial banners, cancel/change plan modals, and usage display; previous threading issues addressed
lib/api/billing.ts Clean API client additions for cancelSubscription and changePlan endpoints with proper typing
lib/plans.ts New shared constants and utilities for plan tiers with sensible defaults

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as OrganizationSettings
    participant API as Billing API
    participant Stripe

    Note over User,Stripe: Change Plan Flow
    User->>UI: Click "Change plan"
    UI->>UI: openChangePlanModal()
    UI->>UI: Set tier from current limit
    User->>UI: Select tier & interval
    User->>UI: Click "Update plan"
    alt hasActiveSubscription
        UI->>API: changePlan(plan_id, interval, limit)
        API->>Stripe: Update subscription
        Stripe-->>API: Success
        API-->>UI: { ok: true }
        UI->>API: getSubscription()
        API-->>UI: Updated subscription
    else No active subscription
        UI->>API: createCheckoutSession(...)
        API->>Stripe: Create checkout session
        Stripe-->>API: { url }
        API-->>UI: Checkout URL
        UI->>User: Redirect to Stripe checkout
    end

    Note over User,Stripe: Cancel Subscription Flow
    User->>UI: Click "Cancel subscription"
    UI->>UI: Show cancel modal
    User->>UI: Choose cancel option
    alt Cancel at period end
        UI->>API: cancelSubscription({ at_period_end: true })
        API->>Stripe: Set cancel_at_period_end
        Stripe-->>API: Success
        API-->>UI: { ok: true, at_period_end: true }
    else Cancel immediately
        UI->>API: cancelSubscription({ at_period_end: false })
        API->>Stripe: Cancel immediately
        Stripe-->>API: Success
        API-->>UI: { ok: true, at_period_end: false }
    end
    UI->>API: getSubscription()
    API-->>UI: Updated subscription with cancel_at_period_end
    UI->>User: Show cancellation banner
Loading

@uz1mani uz1mani merged commit a5c301c into main Feb 9, 2026
1 check passed
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