Skip to content

Commit

Permalink
Extract members-data-api code
Browse files Browse the repository at this point in the history
  • Loading branch information
rupertbates committed Jan 9, 2025
1 parent ca40452 commit 2e05899
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 140 deletions.
120 changes: 120 additions & 0 deletions dotcom-rendering/src/client/userFeatures/membersDataApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { isBoolean, isObject } from '@guardian/libs';
import type { SignedInWithCookies, SignedInWithOkta } from '../../lib/identity';
import { getOptionsHeadersWithOkta } from '../../lib/identity';
import type { UserBenefits } from './user-features-lib';
import { fetchJson } from './user-features-lib';

const dates = {
1: '01',
2: '02',
3: '03',
4: '04',
5: '05',
6: '06',
7: '07',
8: '08',
9: '09',
10: '10',
11: '11',
12: '12',
13: '13',
14: '14',
15: '15',
16: '16',
17: '17',
18: '18',
19: '19',
20: '20',
21: '21',
22: '22',
23: '23',
24: '24',
25: '25',
26: '26',
27: '27',
28: '28',
29: '29',
30: '30',
31: '31',
} as const;

const months = {
1: '01',
2: '02',
3: '03',
4: '04',
5: '05',
6: '06',
7: '07',
8: '08',
9: '09',
10: '10',
11: '11',
12: '12',
} as const;

type LocalDate =
`${number}-${(typeof months)[keyof typeof months]}-${(typeof dates)[keyof typeof dates]}`;

/**
* This type is manually kept in sync with the Membership API:
* https://github.com/guardian/members-data-api/blob/a48acdebed6a334ceb4336ece275b9cf9b3d6bb7/membership-attribute-service/app/models/Attributes.scala#L134-L151
*/
type UserFeaturesResponse = {
userId: string;
tier?: string;
recurringContributionPaymentPlan?: string;
oneOffContributionDate?: LocalDate;
membershipJoinDate?: LocalDate;
digitalSubscriptionExpiryDate?: LocalDate;
paperSubscriptionExpiryDate?: LocalDate;
guardianWeeklyExpiryDate?: LocalDate;
liveAppSubscriptionExpiryDate?: LocalDate;
alertAvailableFor?: string;
showSupportMessaging: boolean;
contentAccess: {
member: boolean;
paidMember: boolean;
recurringContributor: boolean;
digitalPack: boolean;
paperSubscriber: boolean;
guardianWeeklySubscriber: boolean;
};
};

export const syncDataFromMembersDataApi: (
signedInAuthStatus: SignedInWithOkta | SignedInWithCookies,
) => Promise<UserBenefits> = async (
signedInAuthStatus: SignedInWithOkta | SignedInWithCookies,
) => {
const response = await fetchJson(
`${
window.guardian.config.page.userAttributesApiUrl ??
'/USER_ATTRIBUTE_API_NOT_FOUND'
}/me`,
{
mode: 'cors',
...getOptionsHeadersWithOkta(signedInAuthStatus),
},
);
if (!validateResponse(response)) {
throw new Error('invalid response');
}
return {
hideSupportMessaging: !response.showSupportMessaging,
adFree: response.contentAccess.digitalPack,
};
};

const validateResponse = (
response: unknown,
): response is UserFeaturesResponse => {
return (
isObject(response) &&
isBoolean(response.showSupportMessaging) &&
isObject(response.contentAccess) &&
isBoolean(response.contentAccess.paidMember) &&
isBoolean(response.contentAccess.recurringContributor) &&
isBoolean(response.contentAccess.digitalPack)
);
};
85 changes: 3 additions & 82 deletions dotcom-rendering/src/client/userFeatures/user-features-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ const fetchJson = async (
resource: string,
init: RequestInit = {},
): Promise<unknown> => {
if (typeof resource !== 'string') {
throw new Error('First argument should be of type `string`');
}

let path = resource;
if (isPathAbsoluteURL(path)) {
path = window.guardian.config.page.ajaxUrl + resource;
Expand All @@ -86,86 +82,11 @@ const fetchJson = async (
throw new Error(`Fetch error while requesting ${path}: ${resp.statusText}`);
};

const dates = {
1: '01',
2: '02',
3: '03',
4: '04',
5: '05',
6: '06',
7: '07',
8: '08',
9: '09',
10: '10',
11: '11',
12: '12',
13: '13',
14: '14',
15: '15',
16: '16',
17: '17',
18: '18',
19: '19',
20: '20',
21: '21',
22: '22',
23: '23',
24: '24',
25: '25',
26: '26',
27: '27',
28: '28',
29: '29',
30: '30',
31: '31',
} as const;

const months = {
1: '01',
2: '02',
3: '03',
4: '04',
5: '05',
6: '06',
7: '07',
8: '08',
9: '09',
10: '10',
11: '11',
12: '12',
} as const;

type LocalDate =
`${number}-${(typeof months)[keyof typeof months]}-${(typeof dates)[keyof typeof dates]}`;

/**
* This type is manually kept in sync with the Membership API:
* https://github.com/guardian/members-data-api/blob/a48acdebed6a334ceb4336ece275b9cf9b3d6bb7/membership-attribute-service/app/models/Attributes.scala#L134-L151
*/
type UserFeaturesResponse = {
userId: string;
tier?: string;
recurringContributionPaymentPlan?: string;
oneOffContributionDate?: LocalDate;
membershipJoinDate?: LocalDate;
digitalSubscriptionExpiryDate?: LocalDate;
paperSubscriptionExpiryDate?: LocalDate;
guardianWeeklyExpiryDate?: LocalDate;
liveAppSubscriptionExpiryDate?: LocalDate;
alertAvailableFor?: string;
showSupportMessaging: boolean;
contentAccess: {
member: boolean;
paidMember: boolean;
recurringContributor: boolean;
digitalPack: boolean;
paperSubscriber: boolean;
guardianWeeklySubscriber: boolean;
};
export type UserBenefits = {
adFree: boolean;
hideSupportMessaging: boolean;
};

export type { UserFeaturesResponse };

export {
adFreeDataIsPresent,
cookieIsExpiredOrMissing,
Expand Down
15 changes: 13 additions & 2 deletions dotcom-rendering/src/client/userFeatures/user-features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,23 @@ import {
import { refresh } from './user-features';
import { fetchJson } from './user-features-lib';

const fakeUserFeatures = {
showSupportMessaging: false,
contentAccess: {
digitalPack: true,
recurringContributor: false,
paidMember: true,
},
};

jest.mock('./user-features-lib', () => {
// Only mock the fetchJson function, rather than the whole module
const original = jest.requireActual('./user-features-lib');
return {
...original,
fetchJson: jest.fn(() => Promise.resolve()),
fetchJson: jest.fn(() => {
return Promise.resolve(fakeUserFeatures);
}),
};
});

Expand Down Expand Up @@ -81,7 +92,7 @@ describe('Refreshing the features data', () => {
getAuthStatus.mockResolvedValue({
kind: 'SignedInWithOkta',
} as AuthStatus);
fetchJsonSpy.mockReturnValue(Promise.resolve());
fetchJsonSpy.mockReturnValue(Promise.resolve(fakeUserFeatures));
});

it('Performs an update if the user has missing data', async () => {
Expand Down
72 changes: 16 additions & 56 deletions dotcom-rendering/src/client/userFeatures/user-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,25 @@
* https://github.com/guardian/commercial/blob/1a429d6be05657f20df4ca909df7d01a5c3d7402/src/lib/user-features.ts
*/

import {
getCookie,
isBoolean,
isObject,
removeCookie,
setCookie,
} from '@guardian/libs';
import {
getAuthStatus,
getOptionsHeadersWithOkta,
isUserLoggedInOktaRefactor,
} from '../../lib/identity';
import { getCookie, removeCookie, setCookie } from '@guardian/libs';
import { getAuthStatus, isUserLoggedInOktaRefactor } from '../../lib/identity';
import { syncDataFromMembersDataApi } from './membersDataApi';
import type { UserBenefits } from './user-features-lib';
import {
adFreeDataIsPresent,
cookieIsExpiredOrMissing,
fetchJson,
getAdFreeCookie,
setAdFreeCookie,
timeInDaysFromNow,
} from './user-features-lib';
import type { UserFeaturesResponse } from './user-features-lib';

const USER_FEATURES_EXPIRY_COOKIE = 'gu_user_features_expiry';
const HIDE_SUPPORT_MESSAGING_COOKIE = 'gu_hide_support_messaging';
const AD_FREE_USER_COOKIE = 'GU_AF1';
export const USER_FEATURES_EXPIRY_COOKIE = 'gu_user_features_expiry';
export const HIDE_SUPPORT_MESSAGING_COOKIE = 'gu_hide_support_messaging';
export const AD_FREE_USER_COOKIE = 'GU_AF1';

const forcedAdFreeMode = !!/[#&]noadsaf(&.*)?$/.exec(window.location.hash);
export const forcedAdFreeMode = !!/[#&]noadsaf(&.*)?$/.exec(
window.location.hash,
);

const userHasData = () => {
const cookie =
Expand All @@ -39,31 +31,17 @@ const userHasData = () => {
getCookie({ name: HIDE_SUPPORT_MESSAGING_COOKIE });
return !!cookie;
};

const validateResponse = (
response: unknown,
): response is UserFeaturesResponse => {
return (
isObject(response) &&
isBoolean(response.showSupportMessaging) &&
isObject(response.contentAccess) &&
isBoolean(response.contentAccess.paidMember) &&
isBoolean(response.contentAccess.recurringContributor) &&
isBoolean(response.contentAccess.digitalPack)
);
};

const persistResponse = (JsonResponse: UserFeaturesResponse) => {
const persistResponse = (userBenefitsResponse: UserBenefits) => {
setCookie({
name: USER_FEATURES_EXPIRY_COOKIE,
value: timeInDaysFromNow(1),
});
setCookie({
name: HIDE_SUPPORT_MESSAGING_COOKIE,
value: String(!JsonResponse.showSupportMessaging),
value: String(userBenefitsResponse.hideSupportMessaging),
});

if (JsonResponse.contentAccess.digitalPack) {
if (userBenefitsResponse.adFree) {
setAdFreeCookie(2);
} else if (adFreeDataIsPresent() && !forcedAdFreeMode) {
removeCookie({ name: AD_FREE_USER_COOKIE });
Expand All @@ -85,27 +63,9 @@ const requestNewData = () => {
: Promise.reject('The user is not signed in'),
)
.then((signedInAuthStatus) => {
return fetchJson(
`${
window.guardian.config.page.userAttributesApiUrl ??
'/USER_ATTRIBUTE_API_NOT_FOUND'
}/me`,
{
mode: 'cors',
...getOptionsHeadersWithOkta(signedInAuthStatus),
},
)
.then((response) => {
if (!validateResponse(response)) {
throw new Error('invalid response');
}
return response;
})
.then(persistResponse)
.catch(() => {
// eslint-disable-next-line no-console -- error logging
console.error('Error fetching user data');
});
return syncDataFromMembersDataApi(signedInAuthStatus).then(
persistResponse,
);
});
};

Expand Down
Empty file.
2 changes: 2 additions & 0 deletions dotcom-rendering/src/experiments/ab-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { mpuWhenNoEpic } from './tests/mpu-when-no-epic';
import { optimiseSpacefinderInline } from './tests/optimise-spacefinder-inline';
import { signInGateMainControl } from './tests/sign-in-gate-main-control';
import { signInGateMainVariant } from './tests/sign-in-gate-main-variant';
import { userBenefitsApi } from './tests/user-benefits-api';

// keep in sync with ab-tests in frontend
// https://github.com/guardian/frontend/tree/main/static/src/javascripts/projects/common/modules/experiments/ab-tests.ts
Expand All @@ -19,4 +20,5 @@ export const tests: ABTest[] = [
mpuWhenNoEpic,
adBlockAsk,
optimiseSpacefinderInline,
userBenefitsApi,
];
Loading

0 comments on commit 2e05899

Please sign in to comment.