From d39f9231c0ab544d25c00ce34eb3e050401c5d2c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 9 Feb 2026 10:25:10 +0100 Subject: [PATCH] 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