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: [],