Skip to content

Purchase Management: Add manage button to dataviews purchase #103789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/me/purchases/purchase-item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ export function PurchaseItemPaymentMethod( {

if (
purchase.isAutoRenewEnabled &&
! isExpired( purchase ) &&
( ! hasPaymentMethod( purchase ) || isPaidWithCredits( purchase ) ) &&
! isPartnerPurchase( purchase ) &&
! isAkismetFreeProduct( purchase )
Expand Down
8 changes: 3 additions & 5 deletions client/me/purchases/purchases-list-in-dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { recordTracksEvent } from '@automattic/calypso-analytics';
import { CompactCard } from '@automattic/components';
import { SiteDetails } from '@automattic/data-stores';
import { isValueTruthy } from '@automattic/wpcom-checkout';
import { LocalizeProps, localize, useTranslate } from 'i18n-calypso';
import { LocalizeProps, localize } from 'i18n-calypso';
import { Component } from 'react';
import { connect } from 'react-redux';
import noSitesIllustration from 'calypso/assets/images/illustrations/illustration-nosites.svg';
Expand Down Expand Up @@ -63,13 +63,11 @@ function MembershipSubscriptions( {
}: {
memberships: Array< MembershipSubscription >;
} ) {
const translate = useTranslate();

if ( ! memberships.length ) {
return null;
}

return <MembershipsDataViews memberships={ memberships } translate={ translate } />;
return <MembershipsDataViews memberships={ memberships } />;
}

function isDataLoading( {
Expand Down Expand Up @@ -113,7 +111,7 @@ class PurchasesListDataView extends Component<
}

if ( purchases && purchases.length ) {
content = <PurchasesDataViews purchases={ purchases } translate={ translate } />;
content = <PurchasesDataViews purchases={ purchases } />;
}

if ( purchases && ! purchases.length && ! subscriptions.length ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Purchases, SiteDetails } from '@automattic/data-stores';
import { Fields, Operator } from '@wordpress/dataviews';
import { Fields } from '@wordpress/dataviews';
import { LocalizeProps } from 'i18n-calypso';
import { useLocalizedMoment } from 'calypso/components/localized-moment';
import { StoredPaymentMethod } from 'calypso/lib/checkout/payment-methods';
import { getDisplayName, isRenewing, purchaseType } from 'calypso/lib/purchases';
import { getDisplayName, isExpired, isRenewing, purchaseType } from 'calypso/lib/purchases';
import { MembershipSubscription } from 'calypso/lib/purchases/types';
import { useSelector } from 'calypso/state';
import { getSite } from 'calypso/state/sites/selectors';
Expand Down Expand Up @@ -94,9 +94,6 @@ export function getPurchasesFieldDefinitions( {
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: Purchases.Purchase } ) => {
return item.siteName;
},
Expand All @@ -113,9 +110,6 @@ export function getPurchasesFieldDefinitions( {
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: Purchases.Purchase } ) => {
// Render a bunch of things to make this easily searchable.
const site = sites?.[ item.siteId ];
Expand Down Expand Up @@ -153,11 +147,13 @@ export function getPurchasesFieldDefinitions( {
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: Purchases.Purchase } ) => {
return item.expiryStatus;
if ( isExpired( item ) ) {
// Prefix expired items with a z so they sort to the end of the list.
return 'zzz ' + item.expiryStatus + ' ' + item.expiryDate;
}
// Include date in value to sort similar expiries together.
return item.expiryDate + ' ' + item.expiryStatus;
},
render: ( { item }: { item: Purchases.Purchase } ) => {
return (
Expand All @@ -172,11 +168,14 @@ export function getPurchasesFieldDefinitions( {
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: Purchases.Purchase } ) => {
return item.payment.storedDetailsId ?? '';
// Allows sorting by card number or payment partner (eg: `type === 'paypal'`).
return isExpired( item )
? // Do not return card number for expired purchases because it
// will not be displayed so it will look wierd if we sort
// expired purchases with active ones that have the same card.
'expired'
: item.payment.creditCard?.number ?? item.payment.type ?? 'no-payment-method';
},
render: ( { item }: { item: Purchases.Purchase } ) => {
let isBackupMethodAvailable = false;
Expand Down Expand Up @@ -213,11 +212,8 @@ export function getMembershipsFieldDefinitions( {
label: translate( 'Site' ),
type: 'text',
enableGlobalSearch: true,
enableSorting: true,
enableSorting: false,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: MembershipSubscription } ) => {
return item.site_id + ' ' + item.site_title + ' ' + item.site_url;
},
Expand All @@ -237,11 +233,8 @@ export function getMembershipsFieldDefinitions( {
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: MembershipSubscription } ) => {
return item.title;
return item.title + ' ' + item.site_title + ' ' + item.site_url;
},
render: ( { item }: { item: MembershipSubscription } ) => {
return (
Expand All @@ -259,11 +252,8 @@ export function getMembershipsFieldDefinitions( {
label: translate( 'Status' ),
type: 'text',
enableGlobalSearch: true,
enableSorting: true,
enableSorting: false,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: MembershipSubscription } ) => {
return item.end_date ?? '';
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,82 @@
import { Card } from '@automattic/components';
import page from '@automattic/calypso-router';
import { Gridicon, Card } from '@automattic/components';
import { Purchases } from '@automattic/data-stores';
import { DESKTOP_BREAKPOINT } from '@automattic/viewport';
import { useBreakpoint } from '@automattic/viewport-react';
import { DataViews, View, filterSortAndPaginate } from '@wordpress/dataviews';
import { LocalizeProps } from 'i18n-calypso';
import { useMemo, useState } from 'react';
import { useTranslate } from 'i18n-calypso';
import { useEffect, useMemo, useState } from 'react';
import { MembershipSubscription } from 'calypso/lib/purchases/types';
import {
usePurchasesFieldDefinitions,
useMembershipsFieldDefinitions,
} from './hooks/use-field-definitions';

const purchasesDesktopFields = [ 'site', 'product', 'status', 'payment-method' ];
const purchasesMobileFields = [ 'product' ];
export const purchasesDataView: View = {
type: 'table',
page: 1,
perPage: 5,
titleField: 'purchase-id',
showTitle: false,
fields: [ 'site', 'product', 'status', 'payment-method' ],
fields: purchasesDesktopFields,
sort: {
field: 'site',
field: 'product',
direction: 'desc',
},
layout: {},
};

export function PurchasesDataViews( props: {
purchases: Purchases.Purchase[];
translate: LocalizeProps[ 'translate' ];
} ) {
const { purchases } = props;
export function PurchasesDataViews( { purchases }: { purchases: Purchases.Purchase[] } ) {
const isDesktop = useBreakpoint( DESKTOP_BREAKPOINT );
const translate = useTranslate();
const [ currentView, setView ] = useState( purchasesDataView );
// Hide fields at mobile width
useEffect( () => {
if ( isDesktop && currentView.fields === purchasesMobileFields ) {
setView( { ...currentView, fields: purchasesDesktopFields } );
return;
}
if ( ! isDesktop && currentView.fields === purchasesDesktopFields ) {
setView( { ...currentView, fields: purchasesMobileFields } );
return;
}
}, [ isDesktop, currentView, setView ] );
const purchasesDataFields = usePurchasesFieldDefinitions( purchasesDataView.fields );

const { data: adjustedPurchases, paginationInfo } = useMemo( () => {
return filterSortAndPaginate( purchases, currentView, purchasesDataFields );
}, [ purchases, currentView, purchasesDataFields ] );

const actions = useMemo(
() => [
{
id: 'manage-purchase',
label: translate( 'Manage this purchase', { textOnly: true } ),
isPrimary: true,
icon: <Gridicon icon="chevron-right" />,
isEligible: ( item: Purchases.Purchase ) => Boolean( item.domain && item.id ),
callback: ( items: Purchases.Purchase[] ) => {
const siteUrl = items[ 0 ].domain;
const subscriptionId = items[ 0 ].id;
if ( ! siteUrl ) {
// eslint-disable-next-line no-console
console.error( 'Cannot display manage purchase page for subscription without site' );
return;
}
if ( ! subscriptionId ) {
// eslint-disable-next-line no-console
console.error( 'Cannot display manage purchase page for subscription without ID' );
return;
}
page( `/me/purchases/${ siteUrl }/${ subscriptionId }` );
},
},
],
[ translate ]
);

const getItemId = ( item: Purchases.Purchase ) => {
return item.id.toString();
};
Expand All @@ -46,34 +88,69 @@ export function PurchasesDataViews( props: {
view={ currentView }
onChangeView={ setView }
defaultLayouts={ { table: {} } }
actions={ undefined }
actions={ actions }
getItemId={ getItemId }
paginationInfo={ paginationInfo }
/>
</Card>
);
}

const membershipsDesktopFields = [ 'site', 'product', 'status' ];
const membershipsMobileFields = [ 'product' ];
export const membershipDataView: View = {
type: 'table',
page: 1,
perPage: 5,
titleField: 'site',
fields: [ 'product', 'status' ],
titleField: 'purchase-id',
showTitle: false,
fields: membershipsDesktopFields,
sort: {
field: 'site',
field: 'product',
direction: 'desc',
},
layout: {},
};

export function MembershipsDataViews( props: {
memberships: MembershipSubscription[];
translate: LocalizeProps[ 'translate' ];
} ) {
const { memberships } = props;
export function MembershipsDataViews( { memberships }: { memberships: MembershipSubscription[] } ) {
const membershipsDataFields = useMembershipsFieldDefinitions();
const [ currentView, setView ] = useState( purchasesDataView );
const [ currentView, setView ] = useState( membershipDataView );
const isDesktop = useBreakpoint( DESKTOP_BREAKPOINT );
const translate = useTranslate();

// Hide fields at mobile width
useEffect( () => {
if ( isDesktop && currentView.fields === membershipsMobileFields ) {
setView( { ...currentView, fields: membershipsDesktopFields } );
return;
}
if ( ! isDesktop && currentView.fields === membershipsDesktopFields ) {
setView( { ...currentView, fields: membershipsMobileFields } );
return;
}
}, [ isDesktop, currentView, setView ] );

const actions = useMemo(
() => [
{
id: 'manage-purchase',
label: translate( 'Manage this purchase', { textOnly: true } ),
isPrimary: true,
icon: <Gridicon icon="chevron-right" />,
isEligible: ( item: MembershipSubscription ) => Boolean( item.ID ),
callback: ( items: MembershipSubscription[] ) => {
const subscriptionId = items[ 0 ].ID;
if ( ! subscriptionId ) {
// eslint-disable-next-line no-console
console.error( 'Cannot display manage purchase page for subscription without ID' );
return;
}
page( `/me/purchases/other/${ subscriptionId }` );
},
},
],
[ translate ]
);

const { data: adjustedMemberships, paginationInfo } = useMemo( () => {
return filterSortAndPaginate( memberships, currentView, membershipsDataFields );
Expand All @@ -90,7 +167,7 @@ export function MembershipsDataViews( props: {
view={ currentView }
onChangeView={ setView }
defaultLayouts={ { table: {} } }
actions={ undefined }
actions={ actions }
getItemId={ getItemId }
paginationInfo={ paginationInfo }
/>
Expand Down
6 changes: 3 additions & 3 deletions client/me/purchases/purchases-list-in-dataviews/style.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/* Purchase listing
================================================== */
#purchases-list {
padding: 8px 0 0;

.dataviews-view-table,
.dataviews-view-table tr {
max-width: 1040px;
Expand All @@ -18,6 +16,7 @@
.dataviews-view-table tr td:first-child,
.dataviews-view-table tr th:first-child {
padding-left: 24px;
max-width: 250px;
}

.dataviews-view-table__cell-content-wrapper .purchase-item__site {
Expand All @@ -40,7 +39,8 @@
max-width: 462px;
}

.purchase-item__purchase-type, .purchase-item__purchase-type a.purchase-item__link {
.purchase-item__purchase-type,
.purchase-item__purchase-type a.purchase-item__link {
overflow: hidden;
}
}
Expand Down