From d39f9231c0ab544d25c00ce34eb3e050401c5d2c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 10:25:10 +0100 Subject: [PATCH 1/8] feat: add subscription cancellation functionality to OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 110 ++++++++++++++++++- lib/api/billing.ts | 14 +++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index e500209..fa3ae03 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -16,7 +16,7 @@ import { OrganizationInvitation, Organization } from '@/lib/api/organization' -import { getSubscription, createPortalSession, getInvoices, SubscriptionDetails, Invoice } from '@/lib/api/billing' +import { getSubscription, createPortalSession, getInvoices, cancelSubscription, SubscriptionDetails, Invoice } from '@/lib/api/billing' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' @@ -68,6 +68,8 @@ export default function OrganizationSettings() { const [subscription, setSubscription] = useState(null) const [isLoadingSubscription, setIsLoadingSubscription] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) + const [isCanceling, setIsCanceling] = useState(false) + const [showCancelPrompt, setShowCancelPrompt] = useState(false) const [invoices, setInvoices] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) @@ -257,6 +259,20 @@ export default function OrganizationSettings() { } } + const handleCancelSubscription = async (atPeriodEnd: boolean) => { + setIsCanceling(true) + try { + await cancelSubscription({ at_period_end: atPeriodEnd }) + toast.success(atPeriodEnd ? 'Subscription will cancel at the end of the billing period.' : 'Subscription canceled.') + setShowCancelPrompt(false) + loadSubscription() + } catch (error: any) { + toast.error(getAuthErrorMessage(error) || error.message || 'Failed to cancel subscription') + } finally { + setIsCanceling(false) + } + } + const handleDelete = async () => { if (deleteConfirm !== 'DELETE') return @@ -754,6 +770,45 @@ export default function OrganizationSettings() { + {/* Cancel-at-period-end notice or Cancel subscription action */} + {subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && ( + <> + {subscription.cancel_at_period_end ? ( +
+

Subscription set to cancel

+

+ Your subscription will end on{' '} + {(() => { + const raw = subscription.current_period_end + const d = raw ? new Date(raw as string) : null + return raw && d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString() : '—' + })()} + . You can use the app until then. +

+

+ Your data is retained for 30 days after access ends. You can resubscribe anytime from the Stripe portal. +

+
+ ) : ( +
+
+

Cancel subscription

+

+ After cancellation, you can use the app until the end of your billing period. Your data is retained for 30 days after access ends. +

+
+ +
+ )} + + )} + {!subscription.has_payment_method && (

Upgrade to Pro

@@ -1053,6 +1108,59 @@ export default function OrganizationSettings() { )} + + {/* Cancel subscription confirmation modal */} + + {showCancelPrompt && ( + + +
+

Cancel subscription?

+ +
+

+ You can cancel at the end of your billing period (you keep access until then) or cancel immediately. Your data is retained for 30 days after access ends. +

+
+ + + +
+
+
+ )} +
) } diff --git a/lib/api/billing.ts b/lib/api/billing.ts index 29b6d48..46206e4 100644 --- a/lib/api/billing.ts +++ b/lib/api/billing.ts @@ -7,6 +7,8 @@ export interface SubscriptionDetails { billing_interval: string pageview_limit: number has_payment_method: boolean + /** True when subscription is set to cancel at the end of the current period. */ + cancel_at_period_end?: boolean /** Number of sites for the org (billing usage). Present when backend supports usage API. */ sites_count?: number /** Pageviews in current billing period (when pageview_limit > 0). Present when backend supports usage API. */ @@ -50,6 +52,18 @@ export async function createPortalSession(): Promise<{ url: string }> { }) } +export interface CancelSubscriptionParams { + /** If true (default), cancel at end of billing period; if false, cancel immediately. */ + at_period_end?: boolean +} + +export async function cancelSubscription(params?: CancelSubscriptionParams): Promise<{ ok: boolean; at_period_end: boolean }> { + return await billingFetch<{ ok: boolean; at_period_end: boolean }>('/api/billing/cancel', { + method: 'POST', + body: JSON.stringify({ at_period_end: params?.at_period_end ?? true }), + }) +} + export interface CreateCheckoutParams { plan_id: string interval: string From 4ec68e8aaf7bc350dbbdf90264d4fe97abe747ba Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 10:48:55 +0100 Subject: [PATCH 2/8] feat: add change plan functionality to OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 160 ++++++++++++++++--- lib/api/billing.ts | 13 ++ lib/plans.ts | 29 ++++ 3 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 lib/plans.ts diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index fa3ae03..ff5d5af 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -16,7 +16,8 @@ import { OrganizationInvitation, Organization } from '@/lib/api/organization' -import { getSubscription, createPortalSession, getInvoices, cancelSubscription, SubscriptionDetails, Invoice } from '@/lib/api/billing' +import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing' +import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex } from '@/lib/plans' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' @@ -70,6 +71,10 @@ export default function OrganizationSettings() { const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) const [isCanceling, setIsCanceling] = useState(false) const [showCancelPrompt, setShowCancelPrompt] = useState(false) + const [showChangePlanModal, setShowChangePlanModal] = useState(false) + const [changePlanTierIndex, setChangePlanTierIndex] = useState(2) + const [changePlanYearly, setChangePlanYearly] = useState(false) + const [isChangingPlan, setIsChangingPlan] = useState(false) const [invoices, setInvoices] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) @@ -273,6 +278,40 @@ export default function OrganizationSettings() { } } + const openChangePlanModal = () => { + if (subscription?.pageview_limit) { + setChangePlanTierIndex(getTierIndexForLimit(subscription.pageview_limit)) + } else { + setChangePlanTierIndex(2) + } + setChangePlanYearly(subscription?.billing_interval === 'year') + setShowChangePlanModal(true) + } + + const hasActiveSubscription = subscription?.has_payment_method && (subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing') + + const handleChangePlanSubmit = async () => { + const interval = changePlanYearly ? 'year' : 'month' + const limit = getLimitForTierIndex(changePlanTierIndex) + setIsChangingPlan(true) + try { + if (hasActiveSubscription) { + await changePlan({ plan_id: PLAN_ID_SOLO, interval, limit }) + toast.success('Plan updated. Changes may take a moment to reflect.') + setShowChangePlanModal(false) + loadSubscription() + } else { + const { url } = await createCheckoutSession({ plan_id: PLAN_ID_SOLO, interval, limit }) + if (url) window.location.href = url + else throw new Error('No checkout URL') + } + } catch (error: any) { + toast.error(getAuthErrorMessage(error) || error.message || 'Something went wrong.') + } finally { + setIsChangingPlan(false) + } + } + const handleDelete = async () => { if (deleteConfirm !== 'DELETE') return @@ -712,15 +751,24 @@ export default function OrganizationSettings() { - {subscription.has_payment_method && ( - - )} + {subscription.has_payment_method && ( + + )} +
@@ -786,7 +834,7 @@ export default function OrganizationSettings() { . You can use the app until then.

- Your data is retained for 30 days after access ends. You can resubscribe anytime from the Stripe portal. + Your data is retained for 30 days after access ends. You can resubscribe anytime using Change plan above.

) : ( @@ -809,18 +857,6 @@ export default function OrganizationSettings() { )} - {!subscription.has_payment_method && ( -
-

Upgrade to Pro

-

- Get higher limits, more data retention, and priority support. -

- -
- )} - {/* Invoice History */}

Invoice History

@@ -1161,6 +1197,88 @@ export default function OrganizationSettings() { )} + + {/* Change plan modal */} + + {showChangePlanModal && ( + + +
+

Change plan

+ +
+

+ Choose your pageview limit and billing interval. {hasActiveSubscription ? 'Your next invoice will reflect prorations.' : 'You’ll start a new subscription.'} +

+
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+
+ )} +
) } diff --git a/lib/api/billing.ts b/lib/api/billing.ts index 46206e4..447fd23 100644 --- a/lib/api/billing.ts +++ b/lib/api/billing.ts @@ -64,6 +64,19 @@ export async function cancelSubscription(params?: CancelSubscriptionParams): Pro }) } +export interface ChangePlanParams { + plan_id: string + interval: string + limit: number +} + +export async function changePlan(params: ChangePlanParams): Promise<{ ok: boolean }> { + return await billingFetch<{ ok: boolean }>('/api/billing/change-plan', { + method: 'POST', + body: JSON.stringify(params), + }) +} + export interface CreateCheckoutParams { plan_id: string interval: string diff --git a/lib/plans.ts b/lib/plans.ts new file mode 100644 index 0000000..c81a4d5 --- /dev/null +++ b/lib/plans.ts @@ -0,0 +1,29 @@ +/** + * Shared plan and traffic tier definitions for pricing and billing (Change plan). + * Backend supports plan_id "solo" and limit 10k–10M; month/year interval. + */ + +export const PLAN_ID_SOLO = 'solo' + +/** Traffic tiers available for Solo plan (pageview limits). */ +export const TRAFFIC_TIERS = [ + { label: '10k', value: 10000 }, + { label: '50k', value: 50000 }, + { label: '100k', value: 100000 }, + { label: '250k', value: 250000 }, + { label: '500k', value: 500000 }, + { label: '1M', value: 1000000 }, + { label: '2.5M', value: 2500000 }, + { label: '5M', value: 5000000 }, + { label: '10M', value: 10000000 }, +] as const + +export function getTierIndexForLimit(limit: number): number { + const idx = TRAFFIC_TIERS.findIndex((t) => t.value === limit) + return idx >= 0 ? idx : 2 +} + +export function getLimitForTierIndex(index: number): number { + if (index < 0 || index >= TRAFFIC_TIERS.length) return 100000 + return TRAFFIC_TIERS[index].value +} From 29dd20a4a714fac0046cceef973fd0310bda9058 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 11:11:29 +0100 Subject: [PATCH 3/8] Redesign billing tab for clarity and usability - 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 --- components/settings/OrganizationSettings.tsx | 288 +++++++++---------- 1 file changed, 136 insertions(+), 152 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index ff5d5af..435cc1b 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -708,10 +708,10 @@ export default function OrganizationSettings() { )} {activeTab === 'billing' && ( -
+

Billing & Subscription

-

Manage your subscription plan and payment methods.

+

Manage your plan, usage, and payment details.

{isLoadingSubscription ? ( @@ -721,60 +721,82 @@ export default function OrganizationSettings() { ) : !subscription ? (

Could not load subscription details.

- +
) : ( -
- {/* Current Plan */} -
-
-
-

Current Plan

-
- - {subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan +
+ + {/* Trial notice */} + {subscription.subscription_status === 'trialing' && ( +
+
+

+ Your free trial ends on{' '} + + {(() => { + const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null + return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : '—' + })()} - - {subscription.subscription_status === 'trialing' ? 'Trial Active' : (subscription.subscription_status || 'Free')} - -

-
-
- - {subscription.has_payment_method && ( - - )} +

+

+ After the trial you'll be charged automatically unless you cancel before then. +

+ )} -
+ {/* Cancel-at-period-end notice */} + {subscription.cancel_at_period_end && ( +
+

+ Your subscription will end on{' '} + + {(() => { + const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null + return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : '—' + })()} + +

+

+ You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe. +

+
+ )} + + {/* Plan & Usage card */} +
+ {/* Plan header */} +
+
+ + {subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan + + + {subscription.subscription_status === 'trialing' ? 'Trial' : (subscription.subscription_status || 'Free')} + + {subscription.billing_interval && ( + + Billed {subscription.billing_interval}ly + + )} +
+ +
+ + {/* Usage stats */} +
-
Sites
-
+
Sites
+
{typeof subscription.sites_count === 'number' ? subscription.plan_id === 'solo' ? `${subscription.sites_count} / 1` @@ -783,109 +805,81 @@ export default function OrganizationSettings() {
-
Pageviews this period
-
+
Pageviews
+
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' ? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}` : '—'}
-
Billing Interval
-
- {subscription.billing_interval ? `${subscription.billing_interval}ly` : '—'} +
+ {subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
-
-
-
Pageview Limit
-
- {subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / month` : 'Unlimited'} -
-
-
-
- {subscription.subscription_status === 'trialing' ? 'Trial Ends On' : 'Renews On'} -
-
+
{(() => { - const raw = subscription.current_period_end - const d = raw ? new Date(raw as string) : null - const ts = d ? d.getTime() : NaN - return raw && !Number.isNaN(ts) && ts !== 0 ? (d as Date).toLocaleDateString() : '—' + const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null + return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '—' })()}
+
+
Limit
+
+ {subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'} +
+
- {/* Cancel-at-period-end notice or Cancel subscription action */} - {subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && ( - <> - {subscription.cancel_at_period_end ? ( -
-

Subscription set to cancel

-

- Your subscription will end on{' '} - {(() => { - const raw = subscription.current_period_end - const d = raw ? new Date(raw as string) : null - return raw && d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString() : '—' - })()} - . You can use the app until then. -

-

- Your data is retained for 30 days after access ends. You can resubscribe anytime using Change plan above. -

-
- ) : ( -
-
-

Cancel subscription

-

- After cancellation, you can use the app until the end of your billing period. Your data is retained for 30 days after access ends. -

-
- -
- )} - - )} + {/* Quick actions */} +
+ {subscription.has_payment_method && ( + + )} + {subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && !subscription.cancel_at_period_end && ( + + )} +
{/* Invoice History */} -
-

Invoice History

-
- {isLoadingInvoices ? ( -
-
-
- ) : invoices.length === 0 ? ( -
No invoices found.
- ) : ( -
- {invoices.map((invoice) => ( -
-
-
- -
+ {invoices.length > 0 && ( +
+

Recent invoices

+
+ {isLoadingInvoices ? ( +
+
+
+ ) : ( + invoices.map((invoice) => ( +
+
-
+ {(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })} -
-
- {new Date(invoice.created * 1000).toLocaleDateString()} -
+ + + {new Date(invoice.created * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} +
-
- + {invoice.invoice_pdf && ( - - + + )} {invoice.hosted_invoice_url && ( - - + + )}
- ))} -
- )} + )) + )} +
-
+ )}
)}
From f4ee066229e6ed423cab1259104d09ef51bdeadd Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 11:16:23 +0100 Subject: [PATCH 4/8] Make cancel button a clear bordered button instead of a text link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/settings/OrganizationSettings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 435cc1b..a25a129 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -833,7 +833,7 @@ export default function OrganizationSettings() {
{/* Quick actions */} -
+
{subscription.has_payment_method && ( From 55a8cec5af5a6ae8eb4caaf38ff07efcbcd01438 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 14:53:04 +0100 Subject: [PATCH 5/8] refactor: invoice display logic in OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index a25a129..c27aa4b 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -288,7 +288,7 @@ export default function OrganizationSettings() { setShowChangePlanModal(true) } - const hasActiveSubscription = subscription?.has_payment_method && (subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing') + const hasActiveSubscription = subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing' const handleChangePlanSubmit = async () => { const interval = changePlanYearly ? 'year' : 'month' @@ -857,16 +857,17 @@ export default function OrganizationSettings() {
{/* Invoice History */} - {invoices.length > 0 && ( -
-

Recent invoices

-
- {isLoadingInvoices ? ( -
-
-
- ) : ( - invoices.map((invoice) => ( +
+

Recent invoices

+
+ {isLoadingInvoices ? ( +
+
+
+ ) : invoices.length === 0 ? ( +
No invoices found.
+ ) : ( + invoices.map((invoice) => (
@@ -903,11 +904,11 @@ export default function OrganizationSettings() {
)) - )} + ) + }
- )} -
+
)}
)} From ba39fcc458a16cb0dce7e9376288c432b147f12d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 14:57:30 +0100 Subject: [PATCH 6/8] fix: improve member loading state handling in OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index c27aa4b..526d21e 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -130,7 +130,11 @@ export default function OrganizationSettings() { const currentOrgId = getOrgIdFromToken() const loadMembers = useCallback(async () => { - if (!currentOrgId) return + if (!currentOrgId) { + setIsLoadingMembers(false) + return + } + setIsLoadingMembers(true) try { const [membersData, invitesData, orgData] = await Promise.all([ getOrganizationMembers(currentOrgId), @@ -867,8 +871,8 @@ export default function OrganizationSettings() { ) : invoices.length === 0 ? (
No invoices found.
) : ( - invoices.map((invoice) => ( -
+ {invoices.map((invoice) => ( +
@@ -903,7 +907,7 @@ export default function OrganizationSettings() { )}
- )) + ))} ) }
From f390776e11a521f7b7db055a027eaf09ee62453c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 15:06:45 +0100 Subject: [PATCH 7/8] refactor: update subscription cancellation state management in OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 526d21e..d2e1889 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -69,7 +69,7 @@ export default function OrganizationSettings() { const [subscription, setSubscription] = useState(null) const [isLoadingSubscription, setIsLoadingSubscription] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) - const [isCanceling, setIsCanceling] = useState(false) + const [cancelLoadingAction, setCancelLoadingAction] = useState<'period_end' | 'immediate' | null>(null) const [showCancelPrompt, setShowCancelPrompt] = useState(false) const [showChangePlanModal, setShowChangePlanModal] = useState(false) const [changePlanTierIndex, setChangePlanTierIndex] = useState(2) @@ -269,7 +269,7 @@ export default function OrganizationSettings() { } const handleCancelSubscription = async (atPeriodEnd: boolean) => { - setIsCanceling(true) + setCancelLoadingAction(atPeriodEnd ? 'period_end' : 'immediate') try { await cancelSubscription({ at_period_end: atPeriodEnd }) toast.success(atPeriodEnd ? 'Subscription will cancel at the end of the billing period.' : 'Subscription canceled.') @@ -278,12 +278,12 @@ export default function OrganizationSettings() { } catch (error: any) { toast.error(getAuthErrorMessage(error) || error.message || 'Failed to cancel subscription') } finally { - setIsCanceling(false) + setCancelLoadingAction(null) } } const openChangePlanModal = () => { - if (subscription?.pageview_limit) { + if (subscription?.pageview_limit != null && subscription.pageview_limit > 0) { setChangePlanTierIndex(getTierIndexForLimit(subscription.pageview_limit)) } else { setChangePlanTierIndex(2) @@ -1154,7 +1154,7 @@ export default function OrganizationSettings() { @@ -1165,8 +1165,8 @@ export default function OrganizationSettings() {
@@ -1174,11 +1174,12 @@ export default function OrganizationSettings() { variant="ghost" className="text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20" onClick={() => handleCancelSubscription(false)} - disabled={isCanceling} + disabled={cancelLoadingAction != null} + isLoading={cancelLoadingAction === 'immediate'} > Cancel immediately -
From fe6530b464ea225da115eff670798d13427a2d1c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 15:13:12 +0100 Subject: [PATCH 8/8] fix: wrap invoice mapping in fragment to ensure proper rendering in OrganizationSettings component --- components/settings/OrganizationSettings.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index d2e1889..70d613d 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -871,8 +871,9 @@ export default function OrganizationSettings() { ) : invoices.length === 0 ? (
No invoices found.
) : ( - {invoices.map((invoice) => ( -
+ <> + {invoices.map((invoice) => ( +
@@ -908,7 +909,8 @@ export default function OrganizationSettings() {
))} - ) + + ) }