Skip to content
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

Prepare DCR for an AB test between user benefits api & members data api #13113

Merged
merged 6 commits into from
Jan 14, 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
30 changes: 30 additions & 0 deletions dotcom-rendering/src/client/userFeatures/cookies/adFree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getCookie, removeCookie, setCookie } from '@guardian/libs';

const AD_FREE_USER_COOKIE = 'GU_AF1';

export const getAdFreeCookie = (): string | null =>
getCookie({ name: AD_FREE_USER_COOKIE });

/*
* Set the ad free cookie
*
* @param daysToLive - number of days the cookie should be valid
*/
export const setAdFreeCookie = (daysToLive = 1): void => {
const expires = new Date();
expires.setMonth(expires.getMonth() + 6);
setCookie({
name: AD_FREE_USER_COOKIE,
value: expires.getTime().toString(),
rupertbates marked this conversation as resolved.
Show resolved Hide resolved
daysToLive,
});
};

export const adFreeDataIsPresent = (): boolean => {
const cookieVal = getAdFreeCookie();
if (!cookieVal) return false;
return !Number.isNaN(parseInt(cookieVal, 10));
};

export const removeAdFreeCookie = (): void =>
removeCookie({ name: AD_FREE_USER_COOKIE });
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getCookie, removeCookie, setCookie } from '@guardian/libs';

const HIDE_SUPPORT_MESSAGING_COOKIE = 'gu_hide_support_messaging';

export const getHideSupportMessagingCookie = (): string | null =>
getCookie({ name: HIDE_SUPPORT_MESSAGING_COOKIE });

export const setHideSupportMessagingCookie = (value: boolean): void => {
setCookie({
name: HIDE_SUPPORT_MESSAGING_COOKIE,
value: String(value),
});
};

export const removeHideSupportMessagingCookie = (): void =>
removeCookie({ name: HIDE_SUPPORT_MESSAGING_COOKIE });
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getCookie, removeCookie, setCookie } from '@guardian/libs';

const USER_FEATURES_EXPIRY_COOKIE = 'gu_user_features_expiry';

export const getUserFeaturesExpiryCookie = (): string | null =>
getCookie({ name: USER_FEATURES_EXPIRY_COOKIE });

export const setUserFeaturesExpiryCookie = (expiryTime: string): void =>
setCookie({ name: USER_FEATURES_EXPIRY_COOKIE, value: expiryTime });

export const removeUserFeaturesExpiryCookie = (): void =>
removeCookie({ name: USER_FEATURES_EXPIRY_COOKIE });

export const featuresDataIsOld = (): boolean => {
const cookie = getUserFeaturesExpiryCookie();
if (!cookie) return true;
const expiryTime = parseInt(cookie, 10);
const timeNow = new Date().getTime();
return timeNow >= expiryTime;
};
16 changes: 16 additions & 0 deletions dotcom-rendering/src/client/userFeatures/fetchJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const fetchJson = async (
path: string,
init: RequestInit = {},
): Promise<unknown> => {
const resp = await fetch(path, init);
if (resp.ok) {
try {
return resp.json();
} catch (ex) {
throw new Error(
`Fetch error while requesting ${path}: Invalid JSON response`,
);
}
}
throw new Error(`Fetch error while requesting ${path}: ${resp.statusText}`);
};
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 { fetchJson } from './fetchJson';
import type { UserBenefits } from './user-features';

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)
);
};
176 changes: 0 additions & 176 deletions dotcom-rendering/src/client/userFeatures/user-features-lib.ts

This file was deleted.

Loading
Loading