Skip to content

Commit 7faca38

Browse files
642 cache for newsletter subscriptions data (#15024)
* adding caching for newsletter subscriptions * fixing tests * added cache updates wnen user subscribes, added tests * review tweaks * removed empty line * comment fix * cache updates now fetch from the server * fixing issues * fixing test * passing idApiUrl to resolveEmailIfSignedIn * reverting to cache invalidation on new subscription * refactoring * invalidating on sign out click * clearing subscription cache when page is rendered and user signed out
1 parent 0f64327 commit 7faca38

File tree

8 files changed

+346
-56
lines changed

8 files changed

+346
-56
lines changed

dotcom-rendering/src/components/Dropdown.importable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface DropdownLinkType {
3131
isActive?: boolean;
3232
dataLinkName: string;
3333
notifications?: Notification[];
34+
onClick?: () => void;
3435
}
3536

3637
interface Props {
@@ -370,6 +371,7 @@ const DropdownLink = ({ link, index }: DropdownLinkProps) => {
370371
renderingTarget,
371372
);
372373
}
374+
link.onClick?.();
373375
}}
374376
>
375377
<div>

dotcom-rendering/src/components/ManyNewsletterSignUp.importable.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
reportTrackingEvent,
1818
requestMultipleSignUps,
1919
} from '../lib/newsletter-sign-up-requests';
20-
import { useIsSignedIn } from '../lib/useAuthStatus';
20+
import { clearSubscriptionCache } from '../lib/newsletterSubscriptionCache';
21+
import { useAuthStatus, useIsSignedIn } from '../lib/useAuthStatus';
2122
import { useConfig } from './ConfigContext';
2223
import { Flex } from './Flex';
2324
import { ManyNewslettersForm } from './ManyNewslettersForm';
@@ -131,6 +132,7 @@ export const ManyNewsletterSignUp = ({
131132
visibleRecaptcha = false,
132133
}: Props) => {
133134
const isSignedIn = useIsSignedIn();
135+
const authStatus = useAuthStatus();
134136

135137
const [newslettersToSignUpFor, setNewslettersToSignUpFor] = useState<
136138
{
@@ -297,6 +299,11 @@ export const ManyNewsletterSignUp = ({
297299
...(marketingOptIn !== undefined && { marketingOptInType }),
298300
},
299301
);
302+
303+
if (authStatus.kind === 'SignedIn') {
304+
clearSubscriptionCache();
305+
}
306+
300307
setStatus('Success');
301308
};
302309

dotcom-rendering/src/components/SecureSignup.importable.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { useEffect, useRef, useState } from 'react';
2121
import ReactGoogleRecaptcha from 'react-google-recaptcha';
2222
import { submitComponentEvent } from '../client/ophan/ophan';
2323
import { lazyFetchEmailWithTimeout } from '../lib/fetchEmail';
24-
import { useIsSignedIn } from '../lib/useAuthStatus';
24+
import { clearSubscriptionCache } from '../lib/newsletterSubscriptionCache';
25+
import { useAuthStatus, useIsSignedIn } from '../lib/useAuthStatus';
2526
import { palette } from '../palette';
2627
import type { RenderingTarget } from '../types/renderingTarget';
2728
import { useConfig } from './ConfigContext';
@@ -286,6 +287,7 @@ export const SecureSignup = ({
286287
undefined,
287288
);
288289
const isSignedIn = useIsSignedIn();
290+
const authStatus = useAuthStatus();
289291

290292
useEffect(() => {
291293
if (isSignedIn !== 'Pending' && !isSignedIn) {
@@ -327,6 +329,10 @@ export const SecureSignup = ({
327329
setIsWaitingForResponse(false);
328330
setResponseOk(response.ok);
329331

332+
if (response.ok && authStatus.kind === 'SignedIn') {
333+
clearSubscriptionCache();
334+
}
335+
330336
sendTracking(
331337
newsletterId,
332338
response.ok ? 'submission-confirmed' : 'submission-failed',

dotcom-rendering/src/components/TopBar.importable.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Hide } from '@guardian/source/react-components';
1010
import { useEffect, useState } from 'react';
1111
import { addTrackingCodesToUrl } from '../lib/acquisitions';
1212
import type { EditionId } from '../lib/edition';
13+
import { clearSubscriptionCache } from '../lib/newsletterSubscriptionCache';
1314
import { nestedOphanComponents } from '../lib/ophan-helpers';
1415
import { useAuthStatus } from '../lib/useAuthStatus';
1516
import { usePageViewId } from '../lib/usePageViewId';
@@ -124,6 +125,12 @@ export const TopBar = ({
124125
setReferrerUrl(window.location.origin + window.location.pathname);
125126
}, []);
126127

128+
useEffect(() => {
129+
if (authStatus.kind === 'SignedOut') {
130+
clearSubscriptionCache();
131+
}
132+
}, [authStatus.kind]);
133+
127134
const printSubscriptionsHref = addTrackingCodesToUrl({
128135
base: `https://support.theguardian.com/subscribe${
129136
editionId === 'UK' ? '' : '/weekly'

dotcom-rendering/src/components/TopBarMyAccount.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useEffect, useState } from 'react';
1414
import { getZIndex } from '../lib/getZIndex';
1515
import type { SignedIn } from '../lib/identity';
1616
import { createAuthenticationEventParams } from '../lib/identity-component-event';
17+
import { clearSubscriptionCache } from '../lib/newsletterSubscriptionCache';
1718
import {
1819
addNotificationsToDropdownLinks,
1920
mapBrazeCardsToNotifications,
@@ -131,6 +132,9 @@ export const buildIdentityLinks = (
131132

132133
return links.map((link) => ({
133134
...link,
135+
...(link.id === 'sign_out'
136+
? { onClick: () => clearSubscriptionCache() }
137+
: {}),
134138
dataLinkName: nestedOphanComponents(
135139
'header',
136140
'topbar',
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { storage } from '@guardian/libs';
2+
import type { SignedIn } from './identity';
3+
import { getOptionsHeaders } from './identity';
4+
5+
const CACHE_KEY = 'gu.newsletter.subscriptions';
6+
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
7+
8+
interface NewsletterSubscriptionCache {
9+
listIds: number[];
10+
timestamp: number;
11+
userId: string;
12+
}
13+
14+
export const getCachedSubscriptions =
15+
(): NewsletterSubscriptionCache | null => {
16+
try {
17+
const cached = storage.session.get(CACHE_KEY);
18+
if (!cached || typeof cached !== 'string') return null;
19+
20+
const parsed = JSON.parse(cached) as NewsletterSubscriptionCache;
21+
22+
const now = Date.now();
23+
if (now - parsed.timestamp > CACHE_DURATION_MS) {
24+
storage.session.remove(CACHE_KEY);
25+
return null;
26+
}
27+
28+
return parsed;
29+
} catch (error) {
30+
storage.session.remove(CACHE_KEY);
31+
return null;
32+
}
33+
};
34+
35+
export const setCachedSubscriptions = (
36+
listIds: number[],
37+
userId: string,
38+
): void => {
39+
try {
40+
const cache: NewsletterSubscriptionCache = {
41+
listIds,
42+
timestamp: Date.now(),
43+
userId,
44+
};
45+
storage.session.set(CACHE_KEY, JSON.stringify(cache));
46+
} catch (error) {
47+
// Silent failure - cache update is not critical
48+
}
49+
};
50+
51+
export const clearSubscriptionCache = (): void => {
52+
storage.session.remove(CACHE_KEY);
53+
};
54+
55+
export const shouldInvalidateCache = (
56+
cache: NewsletterSubscriptionCache,
57+
currentUserId?: string,
58+
): boolean => {
59+
if (!currentUserId || cache.userId !== currentUserId) {
60+
return true;
61+
}
62+
return false;
63+
};
64+
65+
export interface NewsletterSubscriptionResponse {
66+
result: {
67+
subscriptions: Array<{
68+
listId: string;
69+
}>;
70+
};
71+
}
72+
73+
/**
74+
* Fetches newsletter subscriptions from the Identity API and updates the cache.
75+
*/
76+
export const fetchNewsletterSubscriptions = async (
77+
idApiUrl: string,
78+
userId: string,
79+
authStatus: SignedIn,
80+
): Promise<number[] | null> => {
81+
try {
82+
const options = getOptionsHeaders(authStatus);
83+
const response = await fetch(`${idApiUrl}/users/me/newsletters`, {
84+
method: 'GET',
85+
credentials: 'include',
86+
...options,
87+
});
88+
89+
if (!response.ok) {
90+
return null;
91+
}
92+
93+
const data = (await response.json()) as NewsletterSubscriptionResponse;
94+
const newsletters = data.result.subscriptions;
95+
const listIds = newsletters.map((n) => Number(n.listId));
96+
97+
setCachedSubscriptions(listIds, userId);
98+
return listIds;
99+
} catch (error) {
100+
return null;
101+
}
102+
};

0 commit comments

Comments
 (0)