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

Merged
uz1mani merged 8 commits from staging into main 2026-02-09 14:16:21 +00:00
uz1mani commented 2026-02-09 13:46:48 +00:00 (Migrated from github.com)

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.
## 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.ts` — `TRAFFIC_TIERS`, `PLAN_ID_SOLO`, tier index/limit helpers for pricing and billing. ## Test Plan - [x] Trial user sees trial banner and correct end date; copy about auto-charge. - [x] Cancel at period end and immediately both work; modal copy matches behavior. - [x] Change plan: new subscription → checkout; existing → in-app update and refresh. - [x] “Payment method & invoices” opens Stripe portal when applicable. - [x] Dark/light theme and responsive layout look correct.
greptile-apps[bot] commented 2026-02-09 13:48:43 +00:00 (Migrated from github.com)

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
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> 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. <h3>Confidence Score: 3/5</h3> - 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 <details><summary><h3>Important Files Changed</h3></summary> | 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. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid 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 ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-09 13:48:49 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 13:48:47 +00:00

Incorrect subscription gating
hasActiveSubscription is currently defined as has_payment_method && (active || trialing) (components/settings/OrganizationSettings.tsx:291), which will treat a trialing subscription with no payment method as not active. In that case the Change plan modal will incorrectly route users through createCheckoutSession (components/settings/OrganizationSettings.tsx:303-307) instead of using changePlan, and the modal copy will say “You’ll start a new subscription.” This breaks the stated trial UX for orgs that are trialing before adding a card. Consider basing “existing subscription” on subscription_status alone (or on presence of a subscription id) rather than has_payment_method.

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

Comment:
**Incorrect subscription gating**
`hasActiveSubscription` is currently defined as `has_payment_method && (active || trialing)` (components/settings/OrganizationSettings.tsx:291), which will treat a trialing subscription with no payment method as *not active*. In that case the Change plan modal will incorrectly route users through `createCheckoutSession` (components/settings/OrganizationSettings.tsx:303-307) instead of using `changePlan`, and the modal copy will say “You’ll start a new subscription.” This breaks the stated trial UX for orgs that are trialing before adding a card. Consider basing “existing subscription” on `subscription_status` alone (or on presence of a subscription id) rather than `has_payment_method`.

How can I resolve this? If you propose a fix, please make it concise.
**Incorrect subscription gating** `hasActiveSubscription` is currently defined as `has_payment_method && (active || trialing)` (components/settings/OrganizationSettings.tsx:291), which will treat a trialing subscription with no payment method as *not active*. In that case the Change plan modal will incorrectly route users through `createCheckoutSession` (components/settings/OrganizationSettings.tsx:303-307) instead of using `changePlan`, and the modal copy will say “You’ll start a new subscription.” This breaks the stated trial UX for orgs that are trialing before adding a card. Consider basing “existing subscription” on `subscription_status` alone (or on presence of a subscription id) rather than `has_payment_method`. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 291:291 Comment: **Incorrect subscription gating** `hasActiveSubscription` is currently defined as `has_payment_method && (active || trialing)` (components/settings/OrganizationSettings.tsx:291), which will treat a trialing subscription with no payment method as *not active*. In that case the Change plan modal will incorrectly route users through `createCheckoutSession` (components/settings/OrganizationSettings.tsx:303-307) instead of using `changePlan`, and the modal copy will say “You’ll start a new subscription.” This breaks the stated trial UX for orgs that are trialing before adding a card. Consider basing “existing subscription” on `subscription_status` alone (or on presence of a subscription id) rather than `has_payment_method`. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -797,2 +887,3 @@
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
<div className="flex items-center gap-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium capitalize ${
invoice.status === 'paid'
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 13:48:48 +00:00

Invoices never render while loading
The invoices section is gated by invoices.length > 0 (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially []), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when isLoadingInvoices is true as well (or remove the outer length check).

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

Comment:
**Invoices never render while loading**
The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check).

How can I resolve this? If you propose a fix, please make it concise.
**Invoices never render while loading** The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 859:906 Comment: **Invoices never render while loading** The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check). How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-09 13:52:10 +00:00
uz1mani (Migrated from github.com) commented 2026-02-09 13:52:10 +00:00

Issue: hasActiveSubscription required has_payment_method, so trialing orgs without a card were treated as having no subscription. They were sent to checkout instead of using the in-app change-plan flow.
Fix: hasActiveSubscription is now based only on subscription status:
subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing'. No dependency on has_payment_method.
Why: Trials can exist before a payment method is added. Gating on status (or Stripe subscription id) matches that and keeps trialing users in the correct flow (change plan vs new checkout).

Issue: hasActiveSubscription required has_payment_method, so trialing orgs without a card were treated as having no subscription. They were sent to checkout instead of using the in-app change-plan flow. Fix: hasActiveSubscription is now based only on subscription status: subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing'. No dependency on has_payment_method. Why: Trials can exist before a payment method is added. Gating on status (or Stripe subscription id) matches that and keeps trialing users in the correct flow (change plan vs new checkout).
uz1mani (Migrated from github.com) reviewed 2026-02-09 13:52:22 +00:00
@@ -797,2 +887,3 @@
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
<div className="flex items-center gap-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium capitalize ${
invoice.status === 'paid'
uz1mani (Migrated from github.com) commented 2026-02-09 13:52:22 +00:00

Issue: The invoices block was wrapped in invoices.length > 0, so while invoices was still [] (e.g. during load) the whole block (including spinner and empty state) was hidden.
Fix: The “Recent invoices” block always renders. The inner content is a three-way branch: show spinner when isLoadingInvoices, “No invoices found.” when loaded and invoices.length === 0, otherwise the list. The outer invoices.length > 0 guard was removed.
Why: Users need to see a loading state while invoices are fetched and an explicit empty state when there are none, not a missing section.

Issue: The invoices block was wrapped in invoices.length > 0, so while invoices was still [] (e.g. during load) the whole block (including spinner and empty state) was hidden. Fix: The “Recent invoices” block always renders. The inner content is a three-way branch: show spinner when isLoadingInvoices, “No invoices found.” when loaded and invoices.length === 0, otherwise the list. The outer invoices.length > 0 guard was removed. Why: Users need to see a loading state while invoices are fetched and an explicit empty state when there are none, not a missing section.
greptile-apps[bot] commented 2026-02-09 13:54:41 +00:00 (Migrated from github.com)

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
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> 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. <h3>Confidence Score: 3/5</h3> - 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 <details><summary><h3>Important Files Changed</h3></summary> | 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. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid 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 ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-09 13:54:49 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -714,2 +802,4 @@
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{typeof subscription.sites_count === 'number'
? subscription.plan_id === 'solo'
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 13:54:45 +00:00

Invalid JSX mapping

The invoices branch renders invoices.map(...) as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render.

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

Comment:
**Invalid JSX mapping**

The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render.


How can I resolve this? If you propose a fix, please make it concise.
**Invalid JSX mapping** The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 867:870 Comment: **Invalid JSX mapping** The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] commented 2026-02-09 13:54:51 +00:00 (Migrated from github.com)
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.
<details> <summary>Additional Comments (1)</summary> **`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. <details><summary>Prompt To Fix With AI</summary> `````markdown 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. ````` </details> </details>
uz1mani (Migrated from github.com) reviewed 2026-02-09 13:57:41 +00:00
@@ -714,2 +802,4 @@
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{typeof subscription.sites_count === 'number'
? subscription.plan_id === 'solo'
uz1mani (Migrated from github.com) commented 2026-02-09 13:57:41 +00:00

Issue: The third branch of the ternary was invoices.map(...) without being wrapped in a JS expression. In JSX, that can be treated as a bare token instead of an expression, so the map result may not render correctly.
Fix: Wrapped the map in curly braces so the third branch is { invoices.map((invoice) => ( ... )) }, and adjusted the closing to ))} so the block is valid JSX.
Why: In JSX, only expressions inside { } are evaluated; wrapping the map in braces makes it a proper expression and keeps behavior consistent across tools and React versions.

Issue: The third branch of the ternary was invoices.map(...) without being wrapped in a JS expression. In JSX, that can be treated as a bare token instead of an expression, so the map result may not render correctly. Fix: Wrapped the map in curly braces so the third branch is { invoices.map((invoice) => ( ... )) }, and adjusted the closing to ))} so the block is valid JSX. Why: In JSX, only expressions inside { } are evaluated; wrapping the map in braces makes it a proper expression and keeps behavior consistent across tools and React versions.
greptile-apps[bot] commented 2026-02-09 14:00:45 +00:00 (Migrated from github.com)

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
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> 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 <h3>Confidence Score: 4/5</h3> - 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 <details><summary><h3>Important Files Changed</h3></summary> | 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. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid 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 ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-09 14:00:51 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 3 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -257,6 +268,54 @@ export default function OrganizationSettings() {
}
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 14:00:49 +00:00

Opening modal with stale tier selection if pageview_limit is 0. When subscription?.pageview_limit is 0 or falsy (not just undefined), falls back to tier index 2 (100k). Consider checking for non-zero explicitly: subscription?.pageview_limit > 0.

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

Comment:
Opening modal with stale tier selection if `pageview_limit` is 0. When `subscription?.pageview_limit` is 0 or falsy (not just undefined), falls back to tier index 2 (100k). Consider checking for non-zero explicitly: `subscription?.pageview_limit > 0`.

How can I resolve this? If you propose a fix, please make it concise.
Opening modal with stale tier selection if `pageview_limit` is 0. When `subscription?.pageview_limit` is 0 or falsy (not just undefined), falls back to tier index 2 (100k). Consider checking for non-zero explicitly: `subscription?.pageview_limit > 0`. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 281:288 Comment: Opening modal with stale tier selection if `pageview_limit` is 0. When `subscription?.pageview_limit` is 0 or falsy (not just undefined), falls back to tier index 2 (100k). Consider checking for non-zero explicitly: `subscription?.pageview_limit > 0`. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -719,57 +809,61 @@ export default function OrganizationSettings() {
</div>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 14:00:51 +00:00

Hardcoded 'en-US' locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass undefined or the user's locale to respect their regional formatting preferences.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

Comment:
Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.
Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 875:875 Comment: Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 14:00:49 +00:00

Loading state only shown for "Cancel at period end" button. When user clicks "Cancel immediately", both buttons should be disabled but only the first shows spinner. Consider passing which action is loading, or disable all buttons uniformly.

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

Comment:
Loading state only shown for "Cancel at period end" button. When user clicks "Cancel immediately", both buttons should be disabled but only the first shows spinner. Consider passing which action is loading, or disable all buttons uniformly.

How can I resolve this? If you propose a fix, please make it concise.
Loading state only shown for "Cancel at period end" button. When user clicks "Cancel immediately", both buttons should be disabled but only the first shows spinner. Consider passing which action is loading, or disable all buttons uniformly. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 1163:1176 Comment: Loading state only shown for "Cancel at period end" button. When user clicks "Cancel immediately", both buttons should be disabled but only the first shows spinner. Consider passing which action is loading, or disable all buttons uniformly. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-09 14:06:24 +00:00
@@ -719,57 +809,61 @@ export default function OrganizationSettings() {
</div>
uz1mani (Migrated from github.com) commented 2026-02-09 14:06:24 +00:00

No change: keeping the hardcoded 'en-US' locale for currency formatting.

No change: keeping the hardcoded 'en-US' locale for currency formatting.
uz1mani (Migrated from github.com) reviewed 2026-02-09 14:06:33 +00:00
@@ -257,6 +268,54 @@ export default function OrganizationSettings() {
}
uz1mani (Migrated from github.com) commented 2026-02-09 14:06:33 +00:00

Issue: The condition subscription?.pageview_limit is falsy for 0, so when the limit was 0 we fell back to tier index 2 (100k) and opened the modal with the wrong tier.
Fix: Use an explicit check: subscription?.pageview_limit != null && subscription.pageview_limit > 0 before calling getTierIndexForLimit(subscription.pageview_limit); otherwise keep the default tier index 2.
Why: A real limit of 0 should not be treated as “unknown”; only when there’s no valid positive limit should we use the default.

Issue: The condition subscription?.pageview_limit is falsy for 0, so when the limit was 0 we fell back to tier index 2 (100k) and opened the modal with the wrong tier. Fix: Use an explicit check: subscription?.pageview_limit != null && subscription.pageview_limit > 0 before calling getTierIndexForLimit(subscription.pageview_limit); otherwise keep the default tier index 2. Why: A real limit of 0 should not be treated as “unknown”; only when there’s no valid positive limit should we use the default.
uz1mani (Migrated from github.com) reviewed 2026-02-09 14:06:44 +00:00
uz1mani (Migrated from github.com) commented 2026-02-09 14:06:44 +00:00

Issue: Only the “Cancel at period end” button showed a loading state. For “Cancel immediately”, both actions were disabled but only the first showed a spinner, so it wasn’t clear which action was in progress.
Fix: Replaced the single isCanceling boolean with cancelLoadingAction: 'period_end' | 'immediate' | null. In handleCancelSubscription we set it to 'period_end' or 'immediate' at the start and clear it to null in finally. Both buttons are disabled when cancelLoadingAction != null. The first button uses isLoading={cancelLoadingAction === 'period_end'}, the second uses isLoading={cancelLoadingAction === 'immediate'}. The close and “Keep subscription” buttons use disabled={cancelLoadingAction != null}.
Why: The UI should show which action is in progress and keep all modal actions disabled until that request finishes.

Issue: Only the “Cancel at period end” button showed a loading state. For “Cancel immediately”, both actions were disabled but only the first showed a spinner, so it wasn’t clear which action was in progress. Fix: Replaced the single isCanceling boolean with cancelLoadingAction: 'period_end' | 'immediate' | null. In handleCancelSubscription we set it to 'period_end' or 'immediate' at the start and clear it to null in finally. Both buttons are disabled when cancelLoadingAction != null. The first button uses isLoading={cancelLoadingAction === 'period_end'}, the second uses isLoading={cancelLoadingAction === 'immediate'}. The close and “Keep subscription” buttons use disabled={cancelLoadingAction != null}. Why: The UI should show which action is in progress and keep all modal actions disabled until that request finishes.
greptile-apps[bot] commented 2026-02-09 14:09:53 +00:00 (Migrated from github.com)

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
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> 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 <h3>Confidence Score: 2/5</h3> - 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 <details><summary><h3>Important Files Changed</h3></summary> | 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 | </details> </details> <h3>Sequence Diagram</h3> ```mermaid 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 ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-09 14:09:57 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 1 comment</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-09 14:09:56 +00:00

JSX syntax error: opening brace without matching close

Line 874 has { before invoices.map(...) but the closing is just ) instead of )}. This will cause a compilation error.

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

Comment:
JSX syntax error: opening brace without matching close

Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error.

How can I resolve this? If you propose a fix, please make it concise.
JSX syntax error: opening brace without matching close Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 874:911 Comment: JSX syntax error: opening brace without matching close Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-09 14:13:18 +00:00
uz1mani (Migrated from github.com) commented 2026-02-09 14:13:18 +00:00

Issue: The third branch of the ternary was ( { invoices.map(...) } ). The { before invoices.map was closed by } on the same line as )), and the ) that closes the ternary branch was on the next line. That layout could be parsed as a missing )} and trigger a JSX syntax/compilation error.
Fix: Wrapped the third branch in a fragment so the structure is a single expression:
( <> { invoices.map((invoice) => ( ... )) } </> ).
The { ... } block is clearly closed by )}, and the ternary branch is closed by ) after </>.
Why: Using a fragment makes the ternary’s third branch one JSX node and keeps the expression’s {/} pairing obvious, which avoids the reported syntax/compilation issue.

Issue: The third branch of the ternary was ( { invoices.map(...) } ). The { before invoices.map was closed by } on the same line as )), and the ) that closes the ternary branch was on the next line. That layout could be parsed as a missing )} and trigger a JSX syntax/compilation error. Fix: Wrapped the third branch in a fragment so the structure is a single expression: ( <> { invoices.map((invoice) => ( ... )) } </> ). The { ... } block is clearly closed by )}, and the ternary branch is closed by ) after </>. Why: Using a fragment makes the ternary’s third branch one JSX node and keeps the expression’s {/} pairing obvious, which avoids the reported syntax/compilation issue.
greptile-apps[bot] commented 2026-02-09 14:16:01 +00:00 (Migrated from github.com)

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
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> 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. <h3>Confidence Score: 4/5</h3> - 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 <details><summary><h3>Important Files Changed</h3></summary> | 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 | </details> </details> <h3>Sequence Diagram</h3> ```mermaid 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 ```
Sign in to join this conversation.
No description provided.