Skip to content

Conversation

@tx2pnw
Copy link
Contributor

@tx2pnw tx2pnw commented Dec 23, 2025

Resolves #MON-63

Proposed changes

  • Add expiry date check to MonetizeSubscriptionTerms component in the Hosting Dashboard

  • Add expiry date check to MembershipTerms component in legacy Calypso

  • Use calendar day comparison (isToday() / moment().isSame(day)) instead of 24-hour window for "Expired today" detection

  • Return "Expired today" or "Expired X ago" status with error styling when end_date is in the past, matching existing WPCOM purchase behavior

  • Fix incorrect translator comment that referenced non-existent %(siteUrl)s placeholder

    Why are these changes being made?

When a monetize/membership subscription expires, the UI incorrectly displays "Renews at $X on [past date]" instead of showing "Expired". This is confusing for users who see a renewal date that has already passed. We should basically display this like we display other expired subscriptions in Calypso and the Hosting Dashboard.

The fix adds a simple date comparison before rendering the renewal/expiry text. If the end_date is in the past, we now show "Expired today" or "Expired X ago" with error styling, matching how other expired subscriptions are displayed.

Technical context

Monetize/membership subscriptions use a different data model than WPCOM purchases (plans, domains):

Aspect WPCOM Purchases Monetize Subscriptions
API /upgrades /me/memberships/subscriptions
Expiry handling Calculated expiryStatus field Database status + end_date fields
Expired items Returned with expiryStatus='expired' Still returned if status='active'

For WPCOM purchases, the backend calculates and returns an expiryStatus field that the frontend uses directly. For monetize subscriptions, items with status='active' are returned regardless of whether end_date has passed - the frontend was not checking for this condition.

This fix adds a frontend date comparison to handle the case where status='active' but end_date is in the past. A backend change to calculate an expiryStatus equivalent could be considered for future consistency, but this frontend fix is sufficient for correct display.

Testing instructions

Manual Testing

Prerequisites

  • A WordPress.com account with an expired monetize subscription, OR
    • make a post with paid content block: /wp-admin/post-new.php
    • have secondary user susbscribe to content (open post and subscribe)

Find a Site with Existing Expired Subscription and/or Manipulate data on sandbox:

  • wpcom/public_html/wp-content/lib/billingdaddy/src/api/connected-accounts-namespace.php
    • get_user_subscriptions
     "subscriptions": [
    {
        "ID": "12510333",
        "site_id": "243362578",
        "status": "active",
        "start_date": "2025-01-23 22:52:11",
        "end_date": "2025-12-23 22:52:11",
        "is_renewable": true,
        "renew_interval": "1 month",
        "renewal_price": "5",
        "currency": "USD",
        "product_id": "4088",
        "title": "Monthly Subscription",
        "site_url": "gt7IS-p2",
        "site_title": "Site Title"
    }
    ],

change the following to hard coded values:

  • 'start_date' => '2025-01-23 22:52:11',
  • 'start_date' => '2025-01-23 22:52:11',

Navigate to Hosting Dashboard billing: http://calypso.localhost:3000/me/purchases

  • Scroll to the "Monetize" section
  • Look for subscriptions with dates in the past

Load /me/billing/monetize-subscriptions and validate the same

Before Fix

  • Subscription with past end_date shows: "Renews at $10.00 on January 15, 2024"
  • a

After Fix

  • Same subscription now shows: "Expired today" or "Expired 3 days ago" (in red)
  • b
  • d

Test Cases to Verify

Scenario end_date Viewed Before PR After PR
Expired today Jan 5, 1am Jan 5, 11pm "Renews at $10 on Jan 5" ❌ "Expired today" ✅
Expired yesterday (<24h) Jan 4, 11pm Jan 5, 10am "Renews at $10 on Jan 4" ❌ "Expired 1 day ago" ✅
Expired yesterday (>24h) Jan 4, 3pm Jan 5, 6pm "Renews at $10 on Jan 4" ❌ "Expired 1 day ago" ✅
Expired days ago Jan 1, 5pm Jan 5, 10am "Renews at $10 on Jan 1" ❌ "Expired 4 days ago" ✅
Expired (no renew_interval) Jan 4, 3pm Jan 5, 10am "Expires on Jan 4" ❌ "Expired 1 day ago" ✅
Future + renew_interval Feb 15, 5pm Jan 5, 10am "Renews at $10 on Feb 15" ✅ "Renews at $10 on Feb 15" ✅
Future (no renew_interval) Feb 15, 5pm Jan 5, 10am "Expires on Feb 15" ✅ "Expires on Feb 15" ✅
Never expires null Jan 5, 10am "Never expires" ✅ "Never expires" ✅

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@tx2pnw tx2pnw self-assigned this Dec 23, 2025
@matticbot
Copy link
Contributor

matticbot commented Dec 23, 2025

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~63 bytes added 📈 [gzipped])

Details
name                    parsed_size           gzip_size
entry-dashboard-dotcom       +141 B  (+0.0%)      +63 B  (+0.0%)
entry-dashboard-ciab         +141 B  (+0.0%)      +63 B  (+0.0%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Sections (~273 bytes added 📈 [gzipped])

Details
name                        parsed_size           gzip_size
site-purchases                   +310 B  (+0.0%)      +74 B  (+0.0%)
purchases                        +310 B  (+0.0%)      +74 B  (+0.0%)
staging-site                     +141 B  (+0.0%)      +68 B  (+0.0%)
sites-dashboard                  +141 B  (+0.0%)      +68 B  (+0.0%)
site-settings                    +141 B  (+0.0%)      +68 B  (+0.0%)
site-performance                 +141 B  (+0.0%)      +68 B  (+0.0%)
site-monitoring                  +141 B  (+0.0%)      +68 B  (+0.0%)
site-logs                        +141 B  (+0.0%)      +68 B  (+0.0%)
plans                            +141 B  (+0.0%)      +68 B  (+0.0%)
overview                         +141 B  (+0.0%)      +68 B  (+0.0%)
hosting                          +141 B  (+0.0%)      +68 B  (+0.0%)
github-deployments               +141 B  (+0.0%)      +68 B  (+0.0%)
domains                          +141 B  (+0.0%)      +68 B  (+0.0%)
a8c-for-agencies-referrals       +141 B  (+0.0%)      +67 B  (+0.0%)
a8c-for-agencies-client          +141 B  (+0.0%)      +64 B  (+0.0%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Async-loaded Components (~65 bytes added 📈 [gzipped])

Details
name                                        parsed_size           gzip_size
async-load-calypso-blocks-reader-full-post       +141 B  (+0.3%)      +65 B  (+0.4%)

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@tx2pnw tx2pnw force-pushed the fix/mon-63-expired-monetize-subscription-status branch 2 times, most recently from 43a5bfd to 745f3c4 Compare December 30, 2025 18:39
@matticbot
Copy link
Contributor

matticbot commented Dec 30, 2025

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • help-center
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug fix/mon-63-expired-monetize-subscription-status on your sandbox.

@tx2pnw tx2pnw marked this pull request as ready for review December 30, 2025 18:59
@tx2pnw tx2pnw requested review from a team as code owners December 30, 2025 18:59
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Dec 30, 2025
@tx2pnw tx2pnw changed the title WIP:Show Expired status for monetize subscriptions past their end date Show Expired status for monetize subscriptions past their end date Dec 31, 2025
Copy link
Member

@sirbrillig sirbrillig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely better than what was there before but I think we can be more accurate about the definition of "today" without a large amount of additional work.

@tx2pnw tx2pnw requested a review from sirbrillig January 7, 2026 00:00
@tx2pnw tx2pnw force-pushed the fix/mon-63-expired-monetize-subscription-status branch from 97a8620 to 10205aa Compare January 7, 2026 20:23
Copy link
Member

@sirbrillig sirbrillig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

}

// Check if end_date is in the past (convert to ISO 8601 and append Z to parse as UTC)
const endDate = new Date( subscription.end_date.replace( ' ', 'T' ) + 'Z' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful technique! It may be worth putting it into a helper function somewhere. It looks like regular purchases end up serialized in ISO8601 already (eg: expiry_date: '2026-11-13T00:00:00+00:00') so this might only be important for Monetize purchases. Though if we made a helper it should probably include a little more protection to make sure there's no time zone already (I think maybe just check for a +?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I've added a parseDateAsUTC() helper in datetime.ts. It only"fixes" datetime strings that don't already have timezone info. Anything with Z or +00:00 passes through unchanged to new Date().

  • 2026-01-06 12:00:00 → 2026-01-06T12:00:00Z
  • 2026-01-06 12:00:00.123456 → 2026-01-06T12:00:00.123456Z
  • 2026-01-06T12:00:00 → 2026-01-06T12:00:00Z
  • 2026-01-06T12:00:00Z → pass through
  • 2026-01-06T12:00:00+00:00 → pass through

@tx2pnw tx2pnw force-pushed the fix/mon-63-expired-monetize-subscription-status branch from 10205aa to 35c573f Compare January 7, 2026 22:06
@tx2pnw tx2pnw force-pushed the fix/mon-63-expired-monetize-subscription-status branch from 35c573f to 8ae710d Compare January 7, 2026 22:11
@tx2pnw tx2pnw merged commit c25d83c into trunk Jan 8, 2026
12 checks passed
@tx2pnw tx2pnw deleted the fix/mon-63-expired-monetize-subscription-status branch January 8, 2026 20:51
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jan 8, 2026
heydemoura pushed a commit that referenced this pull request Jan 18, 2026
…107864)

* Show Expired status for monetize subscriptions past their end date

* Show 'Expired X ago' with error styling for expired monetize subscriptions

* Fix 'Expired today' to use calendar day comparison instead of 24-hour window

* Fix timezone parsing for monetize subscription dates

* Add parseDateAsUTC helper to convert MYSQL to ISO8601
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants