src/server/api/ampEpicRouter.ts (238 lines of code) (raw):
import type { ComponentEvent } from '@guardian/ophan-tracker-js';
import cors from 'cors';
import type express from 'express';
import { Router } from 'express';
import {
buildReminderFields,
countryCodeToCountryGroupId,
getLocalCurrencySymbol,
} from '../../shared/lib';
import type { AmountsCardData, AmountsTests, OneOffSignupRequest } from '../../shared/types';
import { getAmpVariantAssignments } from '../lib/ampVariantAssignments';
import { isProd } from '../lib/env';
import type { TickerDataProvider } from '../lib/fetchTickerData';
import { addQueryParams, buildAmpEpicCampaignCode } from '../lib/tracking';
import { ampEpic } from '../tests/amp/ampEpic';
import type { AmpEpicTest } from '../tests/amp/ampEpicModels';
import { getAmpExperimentData } from '../tests/amp/ampEpicSelection';
import type { ValueProvider } from '../utils/valueReloader';
const isSupportUrl = (baseUrl: string): boolean => /\bsupport\./.test(baseUrl);
export const setOneOffReminderEndpoint = (): string =>
isProd
? 'https://support.theguardian.com/reminders/create/one-off'
: 'https://support.code.dev-theguardian.com/reminders/create/one-off';
export const buildAmpEpicRouter = (
choiceCardAmounts: ValueProvider<AmountsTests>,
tickerData: TickerDataProvider,
tests: ValueProvider<AmpEpicTest[]>,
): Router => {
const router = Router();
router.get(
'/experiments_data',
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const response = await getAmpExperimentData(tests.get());
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('Surrogate-Control', 'max-age=0');
res.json(response);
} catch (error) {
next(error);
}
},
);
router.post(
'/set_reminder',
cors({
origin: [
'https://amp-theguardian-com.cdn.ampproject.org',
'https://amp.theguardian.com',
'http://localhost:3030',
'https://amp.code.dev-theguardian.com',
],
credentials: true,
allowedHeaders: ['x-gu-geoip-country-code'],
}),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const { email, reminderDate } = req.body;
const countryCode = req.header('X-GU-GeoIP-Country-Code');
const reminderSignupData: OneOffSignupRequest = {
email: email,
reminderPeriod: reminderDate,
reminderPlatform: 'AMP',
reminderComponent: 'EPIC',
reminderStage: 'PRE',
country: countryCode,
};
const setReminderResponse = await fetch(setOneOffReminderEndpoint(), {
body: JSON.stringify(reminderSignupData),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
res.setHeader('Origin', req.header('Origin') || '*');
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('Surrogate-Control', 'max-age=0');
res.json(setReminderResponse.status);
} catch (error) {
next(error);
}
},
);
router.get(
'/epic',
cors({
origin: [
'https://amp-theguardian-com.cdn.ampproject.org',
'https://amp.theguardian.com',
'http://localhost:3030',
'https://amp.code.dev-theguardian.com',
],
credentials: true,
allowedHeaders: ['x-gu-geoip-country-code'],
}),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
// We use the fastly geo header for determining the correct currency symbol
const countryCode = req.header('X-GU-GeoIP-Country-Code') || 'GB';
const countryGroupId = countryCodeToCountryGroupId(countryCode);
const choiceCardAmountsSettings: AmountsTests = choiceCardAmounts.get();
const ampVariantAssignments = getAmpVariantAssignments(req);
const epic = await ampEpic(
tests.get(),
ampVariantAssignments,
tickerData,
countryCode,
);
const regionAmounts = choiceCardAmountsSettings.find(
(t) =>
t.targeting.targetingType === 'Region' &&
t.targeting.region === countryGroupId,
);
const defaultChoiceCardFrequency =
regionAmounts?.variants[0].defaultContributionType;
const choiceCardAmountsData: AmountsCardData | undefined =
regionAmounts?.variants[0]?.amountsCardData;
const acquisitionData = {
source: 'GOOGLE_AMP',
componentType: 'ACQUISITIONS_EPIC',
componentId: epic.cta.componentId,
campaignCode: epic.cta.campaignCode,
abTest: {
name: epic.testName,
variant: epic.variantName,
},
referrerUrl: req.query.webUrl,
};
const ampState = {
ctaUrl: addQueryParams(
epic.cta.url,
`INTCMP=${
epic.cta.campaignCode
}&acquisitionData=${JSON.stringify(acquisitionData)}`,
),
hidePaymentIcons: !isSupportUrl(epic.cta.url),
reminder: {
...buildReminderFields(),
hideButtons: false,
hideReminderWrapper: true,
hideSuccessMessage: true,
hideFailureMessage: true,
hideReminderCta: !epic.secondaryCta,
hideReminderForm: false,
},
choiceCards:
epic.showChoiceCards &&
choiceCardAmountsData != null &&
defaultChoiceCardFrequency != null
? {
choiceCardSelection: {
frequency: defaultChoiceCardFrequency,
amount: choiceCardAmountsData[defaultChoiceCardFrequency]
.amounts[1],
},
amounts: {
ONE_OFF: choiceCardAmountsData['ONE_OFF'].amounts.slice(0, 2),
MONTHLY: choiceCardAmountsData['MONTHLY'].amounts.slice(0, 2),
ANNUAL: choiceCardAmountsData['ANNUAL'].amounts.slice(0, 2),
},
choiceCardLabelSuffix: {
ONE_OFF: '',
MONTHLY: ' per month',
ANNUAL: ' per year',
},
classNames: {
choiceCard: 'epicChoiceCard',
choiceCardSelected: 'epicChoiceCard epicChoiceCardSelected',
},
currencySymbol: getLocalCurrencySymbol(countryCode),
}
: false,
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('Surrogate-Control', 'max-age=0');
res.json({ ...epic, ...ampState });
} catch (error) {
next(error);
}
},
);
router.get(
'/epic_view', // IMPORTANT: do not change this route!
cors({
origin: [
'https://amp-theguardian-com.cdn.ampproject.org',
'https://amp.theguardian.com',
'http://localhost:3030',
'https://amp.code.dev-theguardian.com',
],
credentials: true,
allowedHeaders: ['x-gu-geoip-country-code'],
}),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('Surrogate-Control', 'max-age=0');
const countryCode = req.header('X-GU-GeoIP-Country-Code');
const ampVariantAssignments = getAmpVariantAssignments(req);
const epic = await ampEpic(
tests.get(),
ampVariantAssignments,
tickerData,
countryCode,
);
const campaignCode = buildAmpEpicCampaignCode(epic.testName, epic.variantName);
const { viewId, ampViewId, browserIdCookie, browserId } = req.query;
const browserIdQuery =
browserIdCookie && browserId ? `&${browserIdCookie}=${browserId}` : '';
const ophanComponentEvent: ComponentEvent = {
component: {
componentType: 'ACQUISITIONS_EPIC',
products: ['CONTRIBUTION', 'MEMBERSHIP_SUPPORTER'],
campaignCode: campaignCode,
id: campaignCode,
},
abTest: {
name: epic.testName,
variant: epic.variantName,
},
action: 'VIEW',
};
const ophanUrl = `https://ophan.theguardian.com/img/2?viewId=${viewId}&ViewId=${ampViewId}${browserIdQuery}&componentEvent=${encodeURI(
JSON.stringify(ophanComponentEvent),
)}`;
fetch(ophanUrl).then((ophanResponse) => {
res.json({
ophanUrl: ophanUrl,
ophanResponseStatus: ophanResponse.status,
});
});
} catch (error) {
next(error);
}
},
);
return router;
};