Skip to content

Commit

Permalink
Newsletter Categories: Add Newsletter Categories to Subscription Mana…
Browse files Browse the repository at this point in the history
…gement page (#81521)

* Rename to useSubscribedNewsletterCategories

* Export getCategoriesKey

* Create useNewsletterCategorySubscriptionMutation

* Fix Subscriber Details displayed categories

* Create SubscribeToNewsletterCategories component

* Fix tests

* Add loading state and feature flag

* Optmize mutation to send only updaded categories

* Extract NewsletterCategoriesResponse

* Fix repeated cache key

* Await query invalidation

* Optmize rendering newsletter categories
  • Loading branch information
phcp authored Sep 11, 2023
1 parent 2cbe34b commit 984e35a
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 34 deletions.
9 changes: 8 additions & 1 deletion client/blocks/reader-site-subscription/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getPaymentInterval,
} from './helpers';
import SiteSubscriptionSettings from './settings';
import SubscribeToNewsletterCategories from './subscribe-to-newsletter-categories';
import './styles.scss';

const SiteSubscriptionDetails = ( {
Expand Down Expand Up @@ -221,11 +222,17 @@ const SiteSubscriptionDetails = ( {
emailMeNewComments={ !! deliveryMethods.email?.send_comments }
/>

{ !! deliveryMethods.email?.send_posts && (
<SubscribeToNewsletterCategories siteId={ blogId } />
) }

<hr className="subscriptions__separator" />

{ /* TODO: Move to SiteSubscriptionInfo component when payment details are in. */ }
<div className="site-subscription-info">
<h2 className="site-subscription-info__heading">{ translate( 'Subscription' ) }</h2>
<h2 className="site-subscription-info__heading">
{ translate( 'Subscription details' ) }
</h2>
<dl className="site-subscription-info__list">
<dt>{ translate( 'Date' ) }</dt>
<dd>
Expand Down
4 changes: 4 additions & 0 deletions client/blocks/reader-site-subscription/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@
}
}
}

&__loading {
margin-bottom: 20px;
}
}

&__unsubscribe-button.components-button {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import config from '@automattic/calypso-config';
import { Spinner } from '@automattic/components';
import { ToggleControl } from '@wordpress/components';
import { useTranslate } from 'i18n-calypso';
import { useMemo } from 'react';
import {
useNewsletterCategorySubscriptionMutation,
useSubscribedNewsletterCategories,
} from 'calypso/data/newsletter-categories';

type SubscribeToNewsletterCategoriesProps = {
siteId: number;
};

const SubscribeToNewsletterCategories = ( { siteId }: SubscribeToNewsletterCategoriesProps ) => {
const translate = useTranslate();
const { data: subscribedNewsletterCategoriesData, isLoading } = useSubscribedNewsletterCategories(
{ siteId }
);
const { mutate, isLoading: isSaving } = useNewsletterCategorySubscriptionMutation( siteId );

const subscribedCategoryIds = useMemo(
() =>
subscribedNewsletterCategoriesData?.newsletterCategories
.filter( ( category ) => !! category.subscribed )
.map( ( category ) => category.id ) || [],
[ subscribedNewsletterCategoriesData ]
);

const handleToggle = ( categoryId: number ) => {
mutate( [
{
term_id: categoryId,
subscribe: ! subscribedCategoryIds.includes( categoryId ),
},
] );
};

if (
! config.isEnabled( 'settings/newsletter-categories' ) ||
! subscribedNewsletterCategoriesData?.enabled
) {
return null;
}

return (
<>
<hr className="subscriptions__separator" />
<div className="site-subscription-info">
<h2 className="site-subscription-info__heading">{ translate( 'Subscription' ) }</h2>

{ isLoading ? (
<div className="site-subscription-info__loading">
<Spinner />
</div>
) : (
<dl className="site-subscription-info__list">
<dt>{ translate( 'Subscribed to' ) }</dt>
<dd>
{ subscribedNewsletterCategoriesData?.newsletterCategories.map(
( newletterCategory ) => (
<div className="setting-item" key={ newletterCategory.id }>
<ToggleControl
checked={ newletterCategory.subscribed }
onChange={ () => handleToggle( newletterCategory.id ) }
disabled={ isSaving }
label={ newletterCategory.name }
/>
<p className="setting-item__hint">
{ translate( 'Receive emails for new posts in %s', {
args: [ newletterCategory.name ],
comment: 'Name of the site that the user tried to resubscribe to.',
} ) }
</p>
</div>
)
) }
</dd>
</dl>
) }
</div>
</>
);
};

export default SubscribeToNewsletterCategories;
2 changes: 2 additions & 0 deletions client/data/newsletter-categories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { default as useNewsletterCategoriesFeatureEnabled } from './use-newslett
export { default as useNewsletterCategoriesQuery } from './use-newsletter-categories-query';
export { default as useMarkAsNewsletterCategoryMutation } from './use-mark-as-newsletter-category-mutation';
export { default as useUnmarkAsNewsletterCategoryMutation } from './use-unmark-as-newsletter-category-mutation';
export { default as useSubscribedNewsletterCategories } from './use-subscribed-newsletter-categories-query';
export { default as useNewsletterCategorySubscriptionMutation } from './use-newsletter-category-subscription-mutation';
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import request from 'wpcom-proxy-request';
import useSubscriberNewsletterCategories from '../use-subscriber-newsletter-categories-query';
import useSubscribedNewsletterCategories from '../use-subscribed-newsletter-categories-query';

jest.mock( 'wpcom-proxy-request', () => jest.fn() );

describe( 'useSubscriberNewsletterCategories', () => {
describe( 'useSubscribedNewsletterCategories', () => {
let queryClient: QueryClient;
let wrapper: any;

Expand Down Expand Up @@ -56,7 +56,7 @@ describe( 'useSubscriberNewsletterCategories', () => {
],
} );

const { result } = renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), {
const { result } = renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), {
wrapper,
} );

Expand Down Expand Up @@ -89,7 +89,7 @@ describe( 'useSubscriberNewsletterCategories', () => {
newsletter_categories: [],
} );

const { result } = renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), {
const { result } = renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), {
wrapper,
} );

Expand All @@ -103,7 +103,7 @@ describe( 'useSubscriberNewsletterCategories', () => {
success: true,
} );

renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), {
renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), {
wrapper,
} );

Expand Down
5 changes: 5 additions & 0 deletions client/data/newsletter-categories/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export type NewsletterCategory = {
parent: number;
subscribed?: boolean;
};

export type NewsletterCategoriesResponse = {
enabled: boolean;
newsletter_categories: NewsletterCategory[];
};
4 changes: 3 additions & 1 deletion client/data/newsletter-categories/use-categories-query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query';
import request from 'wpcom-proxy-request';
import type { Category } from './types';

export const getCategoriesKey = ( siteId?: string | number ) => [ 'categories', siteId ];

const useCategoriesQuery = ( siteId?: string | number ): UseQueryResult< Category[] > => {
return useQuery( {
queryKey: [ 'categories', siteId ],
queryKey: getCategoriesKey( siteId ),
queryFn: () =>
request< Category[] >( {
path: `/sites/${ siteId }/categories`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ const useMarkAsNewsletterCategoryMutation = ( siteId: string | number ) => {
onError: ( error, variables, context ) => {
queryClient.setQueryData( cacheKey, context?.previousData );
},
onSettled: () => {
queryClient.invalidateQueries( cacheKey );
onSettled: async () => {
await queryClient.invalidateQueries( cacheKey );
},
} );
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import request from 'wpcom-proxy-request';
import { NewsletterCategories, NewsletterCategory } from './types';
import { NewsletterCategories, NewsletterCategoriesResponse } from './types';

type NewsletterCategoryQueryProps = {
siteId?: string | number;
};

type NewsletterCategoryResponse = {
enabled: boolean;
newsletter_categories: NewsletterCategory[];
};

const convertNewsletterCategoryResponse = (
response: NewsletterCategoryResponse
export const convertNewsletterCategoriesResponse = (
response: NewsletterCategoriesResponse
): NewsletterCategories => {
return {
enabled: response.enabled,
Expand All @@ -31,11 +26,11 @@ const useNewsletterCategories = ( {
return useQuery( {
queryKey: getNewsletterCategoriesKey( siteId ),
queryFn: () =>
request< NewsletterCategoryResponse >( {
request< NewsletterCategoriesResponse >( {
path: `/sites/${ siteId }/newsletter-categories`,
apiVersion: '2',
apiNamespace: 'wpcom/v2',
} ).then( convertNewsletterCategoryResponse ),
} ).then( convertNewsletterCategoriesResponse ),
enabled: !! siteId,
} );
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import request from 'wpcom-proxy-request';
import { NewsletterCategories, NewsletterCategoriesResponse } from './types';
import { getSubscribedNewsletterCategoriesKey } from './use-subscribed-newsletter-categories-query';

type NewsletterCategorySubscription = {
term_id: number;
subscribe: boolean;
};

const useNewsletterCategorySubscriptionMutation = ( siteId: string | number ) => {
const queryClient = useQueryClient();
const subscribedCategoriesCacheKey = getSubscribedNewsletterCategoriesKey( siteId );

return useMutation( {
mutationFn: async ( categorySubscriptions: NewsletterCategorySubscription[] ) => {
return await request< NewsletterCategoriesResponse >( {
path: `/sites/${ siteId }/newsletter-categories/subscriptions`,
method: 'POST',
apiVersion: '2',
apiNamespace: 'wpcom/v2',
body: { categories: categorySubscriptions },
} );
},
onMutate: async ( categorySubscriptions: NewsletterCategorySubscription[] ) => {
await queryClient.cancelQueries( subscribedCategoriesCacheKey );

const previousData = queryClient.getQueryData< NewsletterCategories >(
subscribedCategoriesCacheKey
);

queryClient.setQueryData(
subscribedCategoriesCacheKey,
( oldData?: NewsletterCategories ) => {
const newSubscribedCategories =
previousData?.newsletterCategories.map( ( category ) => {
const categorySubscription = categorySubscriptions.find(
( subscription ) => subscription.term_id === category.id
);

if ( ! categorySubscription ) {
return category;
}

return {
...category,
subscribed: categorySubscription.subscribe,
};
} ) || [];

const updatedData = {
...oldData,
enabled: oldData?.enabled || false,
newsletterCategories: newSubscribedCategories,
};

return updatedData;
}
);

return { previousData };
},
onError: ( error, variables, context ) => {
queryClient.setQueryData( subscribedCategoriesCacheKey, context?.previousData );
},
onSettled: async () => {
await queryClient.invalidateQueries( subscribedCategoriesCacheKey );
},
} );
};

export default useNewsletterCategorySubscriptionMutation;
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ type NewsletterCategoryResponse = {
newsletter_categories: NewsletterCategory[];
};

export const getSubscriberNewsletterCategoriesKey = (
export const getSubscribedNewsletterCategoriesKey = (
siteId?: string | number,
subscriptionId?: number
) => [ `newsletter-categories`, siteId, subscriptionId ];
) => [ 'subscribed-newsletter-categories', siteId, subscriptionId ];

const convertNewsletterCategoryResponse = (
response: NewsletterCategoryResponse
Expand All @@ -24,12 +24,12 @@ const convertNewsletterCategoryResponse = (
newsletterCategories: response.newsletter_categories,
} );

const useSubscriberNewsletterCategories = ( {
const useSubscribedNewsletterCategories = ( {
siteId,
subscriptionId,
}: NewsletterCategoryQueryProps ): UseQueryResult< NewsletterCategories > => {
return useQuery( {
queryKey: getSubscriberNewsletterCategoriesKey( siteId, subscriptionId ),
queryKey: getSubscribedNewsletterCategoriesKey( siteId, subscriptionId ),
queryFn: () =>
request< NewsletterCategoryResponse >( {
path: `/sites/${ siteId }/newsletter-categories/subscriptions${
Expand All @@ -42,4 +42,4 @@ const useSubscriberNewsletterCategories = ( {
} );
};

export default useSubscriberNewsletterCategories;
export default useSubscribedNewsletterCategories;
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ const useUnmarkAsNewsletterCategoryMutation = ( siteId: string | number ) => {
onError: ( error, variables, context ) => {
queryClient.setQueryData( cacheKey, context?.previousData );
},
onSettled: () => {
queryClient.invalidateQueries( cacheKey );
onSettled: async () => {
await queryClient.invalidateQueries( cacheKey );
},
} );
};
Expand Down
4 changes: 2 additions & 2 deletions client/landing/subscriptions/components/settings/styles.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
@import "@automattic/color-studio/dist/color-variables";
@import "@automattic/typography/styles/variables";

$setting-item-margin-bottom: 16px;
$setting-item-margin-bottom: 20px;

.settings {
.setting-item {
margin-bottom: $setting-item-margin-bottom;

&__hint {
margin-top: 16px;
margin-top: 8px;
color: $studio-gray-50;
font-size: $font-body-extra-small;
text-align: left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ const SubscriberDetails = ( {
const translate = useTranslate();
const subscriptionPlans = useSubscriptionPlans( subscriber );
const newsletterCategoryNames = useMemo(
() => newsletterCategories?.map( ( category ) => category.name ),
() =>
newsletterCategories
?.filter( ( category ) => !! category.subscribed )
.map( ( category ) => category.name ),
[ newsletterCategories ]
);
const { avatar, date_subscribed, display_name, email_address, country, url } = subscriber;
Expand Down
Loading

0 comments on commit 984e35a

Please sign in to comment.