Overview
The cancelSubscription function schedules a subscription to be cancelled at the end of the current billing period. The user retains access until the next billing date, and no refund is issued.
Function Signature
export async function cancelSubscription(props: {
subscriptionId: string;
}): ServerActionRes
Parameters
The ID of the subscription to cancel
Return Value
Indicates whether the operation was successful
Error message if the operation failed. Contains the specific error message from the Dodo Payments API or “An unknown error occurred” for unexpected errors.
Implementation Details
The function:
- Calls the Dodo Payments API to update the subscription with
cancel_at_next_billing_date: true
- Updates the local database to set
cancelAtNextBillingDate: true on the subscription record
- The subscription remains active until the next billing date
- No refund is issued - user retains access for the paid period
- Returns success if both API call and database update complete
Cancellation Behavior
- The subscription is not cancelled immediately
- User retains access until the
nextBillingDate
- No charges occur on the next billing date
- The subscription status changes to cancelled after the billing period ends
- User can restore the subscription before the billing date using
restoreSubscription
Error Handling
- Returns
{ success: false, error: <message> } with the actual error message from Dodo Payments or database
- Returns
{ success: false, error: "An unknown error occurred" } for unexpected errors
- All exceptions are caught and converted to structured error responses
Usage Example
import { cancelSubscription } from '@/actions/cancel-subscription';
import { getUserSubscription } from '@/actions/get-user-subscription';
export default async function CancelHandler() {
// Get current subscription
const subResult = await getUserSubscription();
if (!subResult.success || !subResult.data.subscription) {
return <div>No active subscription to cancel</div>;
}
const subscription = subResult.data.subscription;
// Cancel the subscription
const result = await cancelSubscription({
subscriptionId: subscription.subscriptionId
});
if (!result.success) {
return <div>Error cancelling subscription: {result.error}</div>;
}
return (
<div>
<p>Subscription cancelled successfully</p>
<p>You will retain access until {new Date(subscription.nextBillingDate).toLocaleDateString()}</p>
</div>
);
}
Client Component Example
'use client';
import { cancelSubscription } from '@/actions/cancel-subscription';
import { useState } from 'react';
export function CancelSubscriptionButton({ subscriptionId, nextBillingDate }) {
const [loading, setLoading] = useState(false);
const [cancelled, setCancelled] = useState(false);
const [error, setError] = useState(null);
const handleCancel = async () => {
const confirmed = confirm(
`Are you sure you want to cancel? You'll retain access until ${new Date(nextBillingDate).toLocaleDateString()}.`
);
if (!confirmed) return;
setLoading(true);
setError(null);
const result = await cancelSubscription({ subscriptionId });
if (result.success) {
setCancelled(true);
} else {
setError(result.error);
}
setLoading(false);
};
if (cancelled) {
return (
<div className="text-green-600">
Subscription cancelled. Access until {new Date(nextBillingDate).toLocaleDateString()}
</div>
);
}
return (
<div>
{error && <div className="text-red-600">{error}</div>}
<button
onClick={handleCancel}
disabled={loading}
className="bg-red-600 text-white px-4 py-2 rounded"
>
{loading ? 'Cancelling...' : 'Cancel Subscription'}
</button>
</div>
);
}
'use client';
import { cancelSubscription } from '@/actions/cancel-subscription';
import { useState } from 'react';
export function CancellationFlow({ subscriptionId, nextBillingDate }) {
const [step, setStep] = useState('confirm'); // 'confirm' | 'feedback' | 'done'
const [feedback, setFeedback] = useState('');
const [loading, setLoading] = useState(false);
const handleCancel = async () => {
setLoading(true);
// Submit feedback (implement your own feedback endpoint)
if (feedback) {
await fetch('/api/feedback', {
method: 'POST',
body: JSON.stringify({ feedback, type: 'cancellation' })
});
}
// Cancel subscription
const result = await cancelSubscription({ subscriptionId });
if (result.success) {
setStep('done');
} else {
alert(`Error: ${result.error}`);
}
setLoading(false);
};
if (step === 'confirm') {
return (
<div>
<h2>Cancel Subscription?</h2>
<p>You'll keep access until {new Date(nextBillingDate).toLocaleDateString()}</p>
<button onClick={() => setStep('feedback')}>Continue to Cancel</button>
<button onClick={() => window.history.back()}>Keep Subscription</button>
</div>
);
}
if (step === 'feedback') {
return (
<div>
<h2>We're sorry to see you go</h2>
<p>Would you mind telling us why you're cancelling?</p>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="Your feedback helps us improve..."
rows={4}
/>
<button onClick={handleCancel} disabled={loading}>
{loading ? 'Cancelling...' : 'Confirm Cancellation'}
</button>
<button onClick={handleCancel} disabled={loading}>
Skip & Cancel
</button>
</div>
);
}
return (
<div>
<h2>Subscription Cancelled</h2>
<p>You'll continue to have access until {new Date(nextBillingDate).toLocaleDateString()}</p>
<p>Changed your mind? <a href="/api/restore-subscription">Restore your subscription</a></p>
</div>
);
}
Displaying Cancellation Status
import { getUserSubscription } from '@/actions/get-user-subscription';
export default async function SubscriptionStatus() {
const result = await getUserSubscription();
if (!result.success || !result.data.subscription) {
return <div>No active subscription</div>;
}
const subscription = result.data.subscription;
if (subscription.cancelAtNextBillingDate) {
return (
<div className="border-l-4 border-yellow-500 p-4">
<h3>Subscription Cancelling</h3>
<p>Your subscription will end on {new Date(subscription.nextBillingDate).toLocaleDateString()}</p>
<a href="/api/restore-subscription">Restore Subscription</a>
</div>
);
}
return (
<div>
<p>Active subscription</p>
<p>Next billing: {new Date(subscription.nextBillingDate).toLocaleDateString()}</p>
<a href="/api/cancel-subscription">Cancel</a>
</div>
);
}
Source Code
Location: actions/cancel-subscription.ts:9