Merge pull request #35470 from overleaf/td-stripe-display-past-due

Display subscription for past-due Stripe subscription

GitOrigin-RevId: eb37e56676d15596ae23c0ff3261db89d93d41f3
This commit is contained in:
Tim Down
2026-09-17 11:27:45 +00:00
committed by Copybot
parent 0ec741ad76
commit 61ef838424
4 changed files with 76 additions and 3 deletions
@@ -43,8 +43,12 @@ function PersonalSubscriptionStates({
const { t } = useTranslation()
const state = subscription?.payment.state
if (state === 'active') {
// This version handles subscriptions with and without addons
if (state === 'active' || state === 'past_due') {
// This version handles subscriptions with and without addons.
// Stripe subscriptions in dunning have state 'past_due'; Recurly has
// no equivalent subscription-level state and stays 'active' during
// dunning, so both are rendered the same way here. The past-due banner
// rendered above already covers the difference.
return <ActiveSubscription subscription={subscription} />
} else if (state === 'canceled') {
return <CanceledSubscription subscription={subscription} />
@@ -137,7 +137,9 @@ export function SubscriptionDashboardProvider({
)
const hasValidActiveSubscription = Boolean(
['active', 'canceled'].includes(personalSubscription?.payment?.state) ||
['active', 'canceled', 'past_due'].includes(
personalSubscription?.payment?.state
) ||
institutionMemberships?.length > 0 ||
memberGroupSubscriptions?.length > 0
)
@@ -11,6 +11,7 @@ import {
annualActiveSubscription,
canceledSubscription,
customSubscription,
pastDueActiveSubscription,
pastDueExpiredSubscription,
} from '../../fixtures/subscriptions'
import {
@@ -154,6 +155,17 @@ describe('<PersonalSubscription />', function () {
screen.getByRole('button', { name: 'Reactivate your subscription' })
})
it('renders the active dash for a past-due Stripe subscription', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: pastDueActiveSubscription },
],
})
screen.getByRole('heading', { name: /billing/i })
screen.getByRole('button', { name: 'Cancel your subscription' })
})
it('renders the expired dash', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
@@ -195,6 +207,17 @@ describe('<PersonalSubscription />', function () {
})
expect(invoiceLinks.length).to.equal(2)
})
it('renders subscription details alongside the alert for a past-due Stripe subscription', function () {
renderWithSubscriptionDashContext(<PersonalSubscription />, {
metaTags: [
{ name: 'ol-subscription', value: pastDueActiveSubscription },
],
})
screen.getByRole('alert')
screen.getByRole('heading', { name: /billing/i })
screen.getByRole('button', { name: 'Cancel your subscription' })
})
})
describe('Recurly JS', function () {
@@ -59,6 +59,50 @@ export const annualActiveSubscription: PaidSubscription = {
},
}
export const pastDueActiveSubscription: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
invited_emails: [],
groupPlan: false,
membersLimit: 0,
_id: 'def456',
admin_id: 'abc123',
teamInvites: [],
planCode: 'collaborator-annual',
plan: {
planCode: 'collaborator-annual',
name: 'Standard annual',
price_in_cents: 21900,
annual: true,
featureDescription: [],
canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
billingDetailsLink: '/user/subscription/payment/billing-details',
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'past_due',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: true,
displayPrice: '$199.00',
planOnlyDisplayPrice: '$199.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
isEligibleForDowngradeUpsell: false,
isMigratedFromRecurly: false,
},
}
export const annualActiveSubscriptionWithCoupons: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],