feat: add resume subscription functionality in OrganizationSettings for improved user control over billing

This commit is contained in:
Usman Baig
2026-02-20 16:07:17 +01:00
parent 53ed7493c6
commit 99e9235f1f
2 changed files with 46 additions and 14 deletions

View File

@@ -16,7 +16,7 @@ import {
OrganizationInvitation, OrganizationInvitation,
Organization Organization
} from '@/lib/api/organization' } from '@/lib/api/organization'
import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing' import { getSubscription, createPortalSession, getInvoices, cancelSubscription, resumeSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing'
import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans' import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans'
import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit'
import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings' import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings'
@@ -83,6 +83,7 @@ export default function OrganizationSettings() {
const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false)
const [cancelLoadingAction, setCancelLoadingAction] = useState<'period_end' | 'immediate' | null>(null) const [cancelLoadingAction, setCancelLoadingAction] = useState<'period_end' | 'immediate' | null>(null)
const [showCancelPrompt, setShowCancelPrompt] = useState(false) const [showCancelPrompt, setShowCancelPrompt] = useState(false)
const [isResuming, setIsResuming] = useState(false)
const [showChangePlanModal, setShowChangePlanModal] = useState(false) const [showChangePlanModal, setShowChangePlanModal] = useState(false)
const [changePlanTierIndex, setChangePlanTierIndex] = useState(2) const [changePlanTierIndex, setChangePlanTierIndex] = useState(2)
const [changePlanYearly, setChangePlanYearly] = useState(false) const [changePlanYearly, setChangePlanYearly] = useState(false)
@@ -328,6 +329,19 @@ export default function OrganizationSettings() {
} }
} }
const handleResumeSubscription = async () => {
setIsResuming(true)
try {
await resumeSubscription()
toast.success('Subscription will continue. Cancellation has been undone.')
loadSubscription()
} catch (error: any) {
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to resume subscription')
} finally {
setIsResuming(false)
}
}
const openChangePlanModal = () => { const openChangePlanModal = () => {
if (subscription?.pageview_limit != null && subscription.pageview_limit > 0) { if (subscription?.pageview_limit != null && subscription.pageview_limit > 0) {
setChangePlanTierIndex(getTierIndexForLimit(subscription.pageview_limit)) setChangePlanTierIndex(getTierIndexForLimit(subscription.pageview_limit))
@@ -813,19 +827,30 @@ export default function OrganizationSettings() {
{/* Cancel-at-period-end notice */} {/* Cancel-at-period-end notice */}
{subscription.cancel_at_period_end && ( {subscription.cancel_at_period_end && (
<div className="p-4 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl"> <div className="p-4 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200"> <div>
Your subscription will end on{' '} <p className="text-sm font-medium text-amber-800 dark:text-amber-200">
<span className="font-semibold"> Your subscription will end on{' '}
{(() => { <span className="font-semibold">
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' }) : '—' 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' }) : '—'
</span> })()}
</p> </span>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1"> </p>
You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe. <p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
</p> You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe.
</p>
</div>
<Button
variant="secondary"
onClick={handleResumeSubscription}
disabled={isResuming}
isLoading={isResuming}
className="shrink-0"
>
Keep my subscription
</Button>
</div> </div>
)} )}

View File

@@ -74,6 +74,13 @@ export async function cancelSubscription(params?: CancelSubscriptionParams): Pro
}) })
} }
/** Clears cancel_at_period_end so the subscription continues past the current period. */
export async function resumeSubscription(): Promise<{ ok: boolean }> {
return await billingFetch<{ ok: boolean }>('/api/billing/resume', {
method: 'POST',
})
}
export interface ChangePlanParams { export interface ChangePlanParams {
plan_id: string plan_id: string
interval: string interval: string