diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
index 073d9982a5..1fdc486a31 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
@@ -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
} else if (state === 'canceled') {
return
diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
index a06614ccce..a1d9871692 100644
--- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
+++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
@@ -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
)
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
index 37e2861ec8..45e3bf977a 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
@@ -11,6 +11,7 @@ import {
annualActiveSubscription,
canceledSubscription,
customSubscription,
+ pastDueActiveSubscription,
pastDueExpiredSubscription,
} from '../../fixtures/subscriptions'
import {
@@ -154,6 +155,17 @@ describe('', function () {
screen.getByRole('button', { name: 'Reactivate your subscription' })
})
+ it('renders the active dash for a past-due Stripe subscription', function () {
+ renderWithSubscriptionDashContext(, {
+ 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(, {
metaTags: [
@@ -195,6 +207,17 @@ describe('', function () {
})
expect(invoiceLinks.length).to.equal(2)
})
+
+ it('renders subscription details alongside the alert for a past-due Stripe subscription', function () {
+ renderWithSubscriptionDashContext(, {
+ 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 () {
diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
index 81465a6cb2..bc47626a83 100644
--- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
+++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
@@ -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: [],