---
name: flowglad-subscriptions
description: Manage subscription lifecycle including cancellation, plan changes, reactivation, and status display. Use this skill when users need to upgrade, downgrade, cancel, or reactivate subscriptions.
license: MIT
metadata:
author: flowglad
version: "1.0.0"
---
# Subscriptions Management
## Abstract
This skill covers subscription lifecycle management including cancellation, plan changes, reactivation, trial handling, and status display. Proper subscription management ensures users can upgrade, downgrade, cancel, and reactivate subscriptions with correct billing behavior.
---
## Table of Contents
1. [Reload After Mutations](#1-reload-after-mutations) — **CRITICAL**
- 1.1 [Client-Side State Sync](#11-client-side-state-sync)
- 1.2 [Server-Side Reload Pattern](#12-server-side-reload-pattern)
2. [Cancel Timing Options](#2-cancel-timing-options) — **HIGH**
- 2.1 [End of Period vs Immediate](#21-end-of-period-vs-immediate)
- 2.2 [User Communication](#22-user-communication)
3. [Upgrade vs Downgrade Behavior](#3-upgrade-vs-downgrade-behavior) — **HIGH**
- 3.1 [Immediate Upgrades](#31-immediate-upgrades)
- 3.2 [Deferred Downgrades](#32-deferred-downgrades)
4. [Reactivation with uncancelSubscription](#4-reactivation-with-uncancelsubscription) — **MEDIUM**
- 4.1 [Reactivating Canceled Subscriptions](#41-reactivating-canceled-subscriptions)
5. [Trial Status Detection](#5-trial-status-detection) — **MEDIUM**
- 5.1 [Checking Trial Status](#51-checking-trial-status)
- 5.2 [Trial Expiration Handling](#52-trial-expiration-handling)
6. [Subscription Status Display](#6-subscription-status-display) — **MEDIUM**
- 6.1 [Status Mapping](#61-status-mapping)
- 6.2 [Pending Cancellation Display](#62-pending-cancellation-display)
---
## 1. Reload After Mutations
**Impact: CRITICAL**
After any subscription mutation (cancel, upgrade, downgrade, reactivate), the local billing state is stale. Failing to reload causes UI to show outdated subscription information.
### 1.1 Client-Side State Sync
**Impact: CRITICAL (users see incorrect subscription status)**
When using `useBilling()` on the client, mutations update the server but the local state remains stale until explicitly reloaded.
**Incorrect: assumes state updates automatically**
```tsx
function CancelButton() {
const { cancelSubscription, currentSubscription } = useBilling()
const handleCancel = async () => {
await cancelSubscription({
id: currentSubscription.id,
cancellation: { timing: 'at_end_of_current_billing_period' },
})
// BUG: currentSubscription still shows old status!
// UI will not reflect cancellation until page refresh
}
return (
{/* Shows incorrect status because we didn't reload */}
Status: {currentSubscription?.status}
)
}
```
The UI continues showing the old subscription status because the local `useBilling()` state wasn't refreshed.
**Correct: reload after mutation**
```tsx
function CancelButton() {
const { cancelSubscription, currentSubscription, reload } = useBilling()
const [isLoading, setIsLoading] = useState(false)
const handleCancel = async () => {
setIsLoading(true)
try {
await cancelSubscription({
id: currentSubscription.id,
cancellation: { timing: 'at_end_of_current_billing_period' },
})
// Refresh local state to reflect the cancellation
await reload()
} finally {
setIsLoading(false)
}
}
return (
{/* Now shows correct status after reload */}
Status: {currentSubscription?.status}
)
}
```
### 1.2 Server-Side Reload Pattern
**Impact: CRITICAL (server actions may return stale data)**
When performing mutations server-side and returning billing data to the client, you must fetch fresh data after the mutation.
**Incorrect: returns stale billing data**
```typescript
// Server action
export async function upgradeSubscription(priceSlug: string) {
const session = await auth()
const billing = await flowglad(session.user.id).getBilling()
await billing.adjustSubscription({ priceSlug })
// BUG: billing object still has old data!
return {
success: true,
subscription: billing.currentSubscription, // Stale!
}
}
```
**Correct: fetch fresh billing after mutation**
```typescript
// Server action
export async function upgradeSubscription(priceSlug: string) {
const session = await auth()
const billing = await flowglad(session.user.id).getBilling()
await billing.adjustSubscription({ priceSlug })
// Fetch fresh billing state after mutation
const freshBilling = await flowglad(session.user.id).getBilling()
return {
success: true,
subscription: freshBilling.currentSubscription, // Fresh!
}
}
```
---
## 2. Cancel Timing Options
**Impact: HIGH**
Flowglad supports two cancellation timing modes. Using the wrong mode leads to billing disputes and poor user experience.
### 2.1 End of Period vs Immediate
**Impact: HIGH (billing and access implications)**
Most SaaS applications should cancel at the end of the billing period to let users keep access for time they've paid for.
**Incorrect: immediately cancels without understanding impact**
```typescript
async function handleCancel() {
await billing.cancelSubscription({
id: billing.currentSubscription.id,
// This immediately ends access!
// User loses features they already paid for
cancellation: { timing: 'immediately' },
})
}
```
Immediate cancellation removes access right away, even if the user paid for the full month. This often leads to support tickets and refund requests.
**Correct: cancel at end of period (default for most cases)**
```typescript
async function handleCancel() {
await billing.cancelSubscription({
id: billing.currentSubscription.id,
// User keeps access until their paid period ends
cancellation: { timing: 'at_end_of_current_billing_period' },
})
await billing.reload()
}
```
Use `immediately` only for specific cases like fraud prevention, user request for immediate refund, or account deletion.
### 2.2 User Communication
**Impact: HIGH (user confusion)**
When showing cancellation options, clearly communicate what each timing option means.
**Incorrect: vague cancellation UI**
```tsx
function CancelModal() {
return (
Cancel Subscription
)
}
```
"Cancel Now" and "Cancel Later" don't explain the billing implications.
**Correct: clear communication of timing**
```tsx
function CancelModal() {
const { currentSubscription } = useBilling()
const endDate = currentSubscription?.currentPeriodEnd
return (
Cancel Subscription
You'll keep access until {formatDate(endDate)}.
No further charges will occur.
Access ends now. You may be eligible for a prorated refund.
)
}
```
---
## 3. Upgrade vs Downgrade Behavior
**Impact: HIGH**
Upgrades and downgrades have different default behaviors. Not understanding this leads to incorrect UI and user confusion.
### 3.1 Immediate Upgrades
**Impact: HIGH (billing timing)**
By default, upgrades apply immediately with prorated billing. Users get instant access to the new plan.
**Incorrect: suggests upgrade happens later**
```tsx
function UpgradeButton({ targetPriceSlug }: { targetPriceSlug: string }) {
const { adjustSubscription, reload } = useBilling()
return (
)
}
```
**Correct: communicate immediate effect**
```tsx
function UpgradeButton({ targetPriceSlug }: { targetPriceSlug: string }) {
const { adjustSubscription, reload, getPrice } = useBilling()
const price = getPrice(targetPriceSlug)
return (
Your new plan starts immediately.
You'll be charged a prorated amount for the remainder of this billing period.
)
}
```
### 3.2 Deferred Downgrades
**Impact: HIGH (user expectation mismatch)**
Downgrades typically apply at the end of the current billing period. Users keep their current plan until then.
**Incorrect: implies immediate downgrade**
```tsx
function DowngradeButton({ targetPriceSlug }: { targetPriceSlug: string }) {
const { adjustSubscription, reload } = useBilling()
return (
)
}
```
**Correct: communicate deferred effect**
```tsx
function DowngradeButton({ targetPriceSlug }: { targetPriceSlug: string }) {
const { adjustSubscription, currentSubscription, reload, getPrice } = useBilling()
const price = getPrice(targetPriceSlug)
const endDate = currentSubscription?.currentPeriodEnd
return (
You'll keep your current plan until {formatDate(endDate)}.
Your new plan starts on your next billing date.
)
}
```
---
## 4. Reactivation with uncancelSubscription
**Impact: MEDIUM**
Users who cancel can reactivate their subscription before the cancellation takes effect. This must be handled with the correct API.
### 4.1 Reactivating Canceled Subscriptions
**Impact: MEDIUM (reactivation flow)**
A subscription canceled with `at_end_of_current_billing_period` can be reactivated until the period ends.
**Incorrect: tries to reactivate by creating new checkout**
```tsx
function ReactivateButton() {
const { currentSubscription, createCheckoutSession } = useBilling()
// Subscription is set to cancel at period end
const isPendingCancel = currentSubscription?.cancelAtPeriodEnd
if (!isPendingCancel) return null
return (
)
}
```
**Correct: use uncancelSubscription**
```tsx
function ReactivateButton() {
const { currentSubscription, uncancelSubscription, reload } = useBilling()
const [isLoading, setIsLoading] = useState(false)
// Subscription is set to cancel at period end
const isPendingCancel = currentSubscription?.cancelAtPeriodEnd
if (!isPendingCancel) return null
const handleReactivate = async () => {
setIsLoading(true)
try {
await uncancelSubscription({
id: currentSubscription.id,
})
await reload()
} finally {
setIsLoading(false)
}
}
return (
Your subscription is set to cancel on {formatDate(currentSubscription.currentPeriodEnd)}
)
}
```
Note: Reactivation only works for subscriptions canceled with `at_end_of_current_billing_period`. Immediately canceled subscriptions cannot be reactivated this way.
---
## 5. Trial Status Detection
**Impact: MEDIUM**
Users on trial subscriptions have different needs than paying subscribers. Proper trial detection enables targeted UI and messaging.
### 5.1 Checking Trial Status
**Impact: MEDIUM (trial-specific UI)**
**Incorrect: ignores trial status**
```tsx
function SubscriptionBanner() {
const { currentSubscription } = useBilling()
if (!currentSubscription) {
return
No active subscription
}
// Doesn't distinguish between trial and paid
return
You're on the {currentSubscription.product.name} plan
}
```
**Correct: check trial status**
```tsx
function SubscriptionBanner() {
const { currentSubscription, loaded } = useBilling()
if (!loaded) return
if (!currentSubscription) {
return