Skip to content

Commit c25d83c

Browse files
authored
Show Expired status for monetize subscriptions past their end date (#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
1 parent 2dae959 commit c25d83c

File tree

3 files changed

+61
-6
lines changed

3 files changed

+61
-6
lines changed

client/dashboard/me/billing-monetize-subscriptions/monetize-item.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { useQuery } from '@tanstack/react-query';
66
import { ExternalLink } from '@wordpress/components';
77
import { createInterpolateElement } from '@wordpress/element';
88
import { __, sprintf } from '@wordpress/i18n';
9+
import { isToday } from 'date-fns';
910
import SiteIcon from '../../components/site-icon';
10-
import { formatDate } from '../../utils/datetime';
11+
import { Text } from '../../components/text';
12+
import { formatDate, getRelativeTimeString, parseDateAsUTC } from '../../utils/datetime';
1113

1214
export const MonetizeSubscriptionTerms = ( {
1315
subscription,
@@ -20,19 +22,36 @@ export const MonetizeSubscriptionTerms = ( {
2022
return <>{ __( 'Never expires' ) }</>;
2123
}
2224

25+
// Check if end_date is in the past
26+
const endDate = parseDateAsUTC( subscription.end_date );
27+
const isExpired = endDate < new Date();
28+
29+
// Show "Expired" for past dates
30+
if ( isExpired ) {
31+
const isExpiredToday = isToday( endDate );
32+
const expiredTodayText = __( 'Expired today' );
33+
// translators: timeSinceExpiry is of the form "[number] [time-period] ago" i.e. "3 days ago"
34+
const expiredFromNowText = sprintf( __( 'Expired %(timeSinceExpiry)s' ), {
35+
timeSinceExpiry: getRelativeTimeString( endDate ),
36+
} );
37+
38+
return <Text intent="error">{ isExpiredToday ? expiredTodayText : expiredFromNowText }</Text>;
39+
}
40+
41+
// Show renewal or expiry for future dates
2342
return (
2443
<>
2544
{ subscription.renew_interval === null
2645
? // translators: %(date)s is the date the subscription expires. Format is LL (e.g. January 1, 2020).
2746
sprintf( __( 'Expires on %(date)s' ), {
28-
date: formatDate( new Date( Date.parse( subscription?.end_date ?? '' ) ), locale, {
47+
date: formatDate( endDate, locale, {
2948
dateStyle: 'long',
3049
} ),
3150
} )
32-
: // translators: %(siteUrl)s is the URL of the site. %(date)s is the date the subscription renews. . Format is LL (e.g. January 1, 2020).
51+
: // translators: %(amount)s is the renewal price, %(date)s is the date the subscription renews. Format is LL (e.g. January 1, 2020).
3352
sprintf( __( 'Renews at %(amount)s on %(date)s' ), {
3453
amount: formatCurrency( Number( subscription.renewal_price ), subscription.currency ),
35-
date: formatDate( new Date( Date.parse( subscription?.end_date ?? '' ) ), locale, {
54+
date: formatDate( endDate, locale, {
3655
dateStyle: 'long',
3756
} ),
3857
} ) }

client/dashboard/utils/datetime.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,17 @@ export function formatSiteYmd( date: Date ) {
303303
const day = String( date.getDate() ).padStart( 2, '0' );
304304
return `${ year }-${ month }-${ day }`;
305305
}
306+
307+
/**
308+
* Parse a date string as UTC, handling MySQL and ISO8601 formats.
309+
*/
310+
export function parseDateAsUTC( dateString: string ): Date {
311+
// Handle datetime without timezone: "YYYY-MM-DD HH:MM:SS" or "YYYY-MM-DDTHH:MM:SS" (optional microseconds)
312+
const match = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/.exec( dateString.trim() );
313+
if ( match ) {
314+
return new Date( `${ match[ 1 ] }T${ match[ 2 ] }Z` );
315+
}
316+
317+
// Everything else - pass through unchanged
318+
return new Date( dateString );
319+
}

client/me/purchases/membership-item/index.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,40 @@ export const MembershipTerms = ( { subscription }: { subscription: MembershipSub
1616
return <>{ translate( 'Never expires' ) }</>;
1717
}
1818

19+
// Check if expired (parse as UTC since end_date is stored in UTC, then convert to local for display)
20+
const endDate = moment.utc( subscription.end_date ).local();
21+
const isExpired = endDate.isBefore( moment() );
22+
23+
if ( isExpired ) {
24+
const isExpiredToday = moment().isSame( endDate, 'day' );
25+
26+
return (
27+
<span className="purchase-item__is-error">
28+
{ isExpiredToday
29+
? translate( 'Expired today' )
30+
: translate( 'Expired %(timeSinceExpiry)s', {
31+
args: {
32+
timeSinceExpiry: endDate.fromNow(),
33+
},
34+
context:
35+
'timeSinceExpiry is of the form "[number] [time-period] ago" i.e. "3 days ago"',
36+
} ) }
37+
</span>
38+
);
39+
}
40+
1941
return (
2042
<>
2143
{ subscription.renew_interval === null
2244
? translate( 'Expires on %(date)s', {
2345
args: {
24-
date: moment( subscription.end_date ).format( 'LL' ),
46+
date: endDate.format( 'LL' ),
2547
},
2648
} )
2749
: translate( 'Renews at %(amount)s on %(date)s', {
2850
args: {
2951
amount: formatCurrency( Number( subscription.renewal_price ), subscription.currency ),
30-
date: moment( subscription.end_date ).format( 'LL' ),
52+
date: endDate.format( 'LL' ),
3153
},
3254
} ) }
3355
</>

0 commit comments

Comments
 (0)