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

WIP - landing page config #6655

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
83 changes: 83 additions & 0 deletions support-frontend/app/admin/settings/LandingPageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package admin.settings

import com.gu.support.encoding.Codec
import com.gu.support.encoding.Codec.deriveCodec
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.{Decoder, Encoder}

sealed trait Status
object Status {
case object Live extends Status
case object Draft extends Status

implicit val decoder: Decoder[Status] = Decoder[String].emap {
case "Live" => Right(Live)
case "Draft" => Right(Draft)
case other => Left(s"Invalid Status: $other")
}
implicit val encoder: Encoder[Status] = Encoder[String].contramap { s =>
println(s"encoding $s")
s match {
case Live => "Live"
case Draft => "Draft"
}
}
}

case class LandingPageTestTargeting(
countryGroups: List[String],
)

object LandingPageTestTargeting {
implicit val codec: Codec[LandingPageTestTargeting] = deriveCodec
}

case class LandingPageCopy(
heading: String,
subheading: String,
)

object LandingPageCopy {
implicit val codec: Codec[LandingPageCopy] = deriveCodec
}

case class LandingPageVariant(
name: String,
copy: LandingPageCopy,
)

object LandingPageVariant {
implicit val codec: Codec[LandingPageVariant] = deriveCodec
}

case class LandingPageTest(
name: String,
status: Status,
targeting: LandingPageTestTargeting,
variants: List[LandingPageVariant],
)

object LandingPageTest {
implicit val encoder: Encoder[LandingPageTest] = deriveEncoder
implicit val decoder: Decoder[LandingPageTest] = deriveDecoder
}

// TODO - fetch config from dynamodb
object LandingPageTestsProvider extends SettingsProvider[List[LandingPageTest]] {
def settings(): List[LandingPageTest] = List(
LandingPageTest(
name = "MY_TEST",
status = Status.Live,
targeting = LandingPageTestTargeting(countryGroups = List("GBPCountries")),
variants = List(
LandingPageVariant(
name = "CONTROL",
copy = LandingPageCopy(
heading = "My heading",
subheading = "Subheading",
),
),
),
),
)
}
1 change: 1 addition & 0 deletions support-frontend/app/admin/settings/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ case class AllSettings(
amounts: AmountsTests,
contributionTypes: ContributionTypes,
metricUrl: MetricUrl,
landingPageTests: List[LandingPageTest],
)

object AllSettings {
Expand Down
3 changes: 3 additions & 0 deletions support-frontend/app/admin/settings/SettingsProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AllSettingsProvider private (
amountsProvider: SettingsProvider[AmountsTests],
contributionTypesProvider: SettingsProvider[ContributionTypes],
metricUrl: MetricUrl,
landingPageTestsProvider: SettingsProvider[List[LandingPageTest]],
) {

def getAllSettings(): AllSettings = {
Expand All @@ -40,6 +41,7 @@ class AllSettingsProvider private (
amountsProvider.settings(),
contributionTypesProvider.settings(),
metricUrl,
landingPageTestsProvider.settings(),
)
}
}
Expand All @@ -58,6 +60,7 @@ object AllSettingsProvider {
amountsProvider,
contributionTypesProvider,
config.metricUrl,
LandingPageTestsProvider,
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion support-frontend/assets/helpers/abTests/abtest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const MVT_COOKIE = 'GU_mvt_id';
const MVT_MAX = 1_000_000;

// Attempts to retrieve the MVT id from a cookie, or sets it.
function getMvtId(): number {
export function getMvtId(): number {
const mvtIdCookieValue = cookie.get(MVT_COOKIE);
let mvtId = Number(mvtIdCookieValue);

Expand Down
51 changes: 51 additions & 0 deletions support-frontend/assets/helpers/abTests/landingPageAbTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import seedrandom from 'seedrandom';
import type {
LandingPageTest,
LandingPageVariant,
} from '../globalsAndSwitches/landingPageSettings';
import type { CountryGroupId } from '../internationalisation/countryGroup';
import { getMvtId } from './abtest';

export type LandingPageSelection = LandingPageVariant & { testName: string };

export const fallBackLandingPageSelection: LandingPageSelection = {
testName: 'FALLBACK_LANDING_PAGE',
name: 'CONTROL',
copy: {
heading: 'Support fearless, independent journalism',
subheading:
"We're not owned by a billionaire or shareholders - our readers support us. Choose to join with one of the options below. Cancel anytime.",
},
};

function randomNumber(mvtId: number, seed: string): number {
const rng = seedrandom(mvtId + seed);
return Math.abs(rng.int32());
}

export function getLandingPageSettings(
countryGroupId: CountryGroupId,
tests: LandingPageTest[] = [],
mvtId: number = getMvtId(),
): LandingPageVariant & { testName: string } {
const test = tests
.filter((test) => test.status == 'Live')
.find((test) => {
const { countryGroups } = test.targeting;
return countryGroups.includes(countryGroupId);
});

if (test) {
const idx = randomNumber(mvtId, test.name) % test.variants.length;
const variant = test.variants[idx];

/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- extra cautious */
if (variant) {
return {
testName: test.name,
...variant,
};
}
}
return fallBackLandingPageSelection;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { CountryGroupId } from '../internationalisation/countryGroup';

interface LandingPageCopy {
heading: string;
subheading: string;
}

export interface LandingPageVariant {
name: string;
copy: LandingPageCopy;
}

interface LandingPageTestTargeting {
countryGroups: CountryGroupId[];
}

export interface LandingPageTest {
name: string;
status: 'Live' | 'Draft';
targeting: LandingPageTestTargeting;
variants: LandingPageVariant[];
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AmountsTests, ContributionTypes } from 'helpers/contributions';
import 'helpers/contributions';
import type { LandingPageTest } from './landingPageSettings';

export type Status = 'On' | 'Off';

Expand Down Expand Up @@ -28,4 +29,5 @@ export type Settings = {
amounts?: AmountsTests;
contributionTypes: ContributionTypes;
metricUrl: string;
landingPageTests?: LandingPageTest[];
};
2 changes: 2 additions & 0 deletions support-frontend/assets/helpers/redux/commonState/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const commonSlice = createSlice({
abParticipations,
settings,
amounts,
landingPageSettings,
} = action.payload;
return {
...state,
Expand All @@ -44,6 +45,7 @@ export const commonSlice = createSlice({
abParticipations,
settings,
amounts,
landingPageSettings,
};
},
setCountryInternationalisation(state, action: PayloadAction<IsoCountry>) {
Expand Down
5 changes: 5 additions & 0 deletions support-frontend/assets/helpers/redux/commonState/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
Campaign,
ReferrerAcquisitionData,
} from 'helpers/tracking/acquisitions';
import type { LandingPageSelection } from '../../abTests/landingPageAbTests';
import { fallBackLandingPageSelection } from '../../abTests/landingPageAbTests';

export type Internationalisation = {
currencyId: IsoCurrency;
Expand All @@ -30,6 +32,7 @@ export type CommonState = {
settings: Settings;
amounts: SelectedAmountsVariant;
internationalisation: Internationalisation;
landingPageSettings: LandingPageSelection;
};

export type CommonStateSetupData = {
Expand All @@ -40,6 +43,7 @@ export type CommonStateSetupData = {
abParticipations: Participations;
settings: Settings;
amounts: SelectedAmountsVariant;
landingPageSettings: LandingPageSelection;
};

const countryGroupId = CountryGroup.detect();
Expand All @@ -58,4 +62,5 @@ export const initialCommonState: CommonState = {
useLocalCurrency: false,
defaultCurrency: 'USD',
},
landingPageSettings: fallBackLandingPageSelection,
};
11 changes: 11 additions & 0 deletions support-frontend/assets/helpers/redux/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
getReferrerAcquisitionData,
} from 'helpers/tracking/acquisitions';
import { getAllQueryParamsWithExclusions } from 'helpers/urls/url';
import type { LandingPageSelection } from '../../abTests/landingPageAbTests';
import { getLandingPageSettings } from '../../abTests/landingPageAbTests';
import type { SelectedAmountsVariant } from '../../contributions';
import type { CommonState, Internationalisation } from '../commonState/state';

Expand All @@ -27,6 +29,7 @@ function buildInitialState(
settings: Settings,
acquisitionData: ReferrerAcquisitionData,
amounts: SelectedAmountsVariant,
landingPageSettings: LandingPageSelection,
): CommonState {
const excludedParameters = ['REFPVID', 'INTCMP', 'acquisitionData'];
const otherQueryParams = getAllQueryParamsWithExclusions(excludedParameters);
Expand All @@ -46,6 +49,7 @@ function buildInitialState(
abParticipations,
settings,
amounts,
landingPageSettings,
};
}

Expand All @@ -57,6 +61,11 @@ export function getInitialState(): CommonState {
const { selectedAmountsVariant, amountsParticipation } =
getAmountsTestVariant(countryId, countryGroupId, settings);

const landingPageSettings = getLandingPageSettings(
countryGroupId,
settings.landingPageTests,
);

const abtestInitalizerData = {
countryId,
countryGroupId,
Expand All @@ -67,6 +76,7 @@ export function getInitialState(): CommonState {
const participationsWithAmountsTest = {
...participations,
...amountsParticipation,
[landingPageSettings.testName]: landingPageSettings.name,
};

const acquisitionData: ReferrerAcquisitionData = getReferrerAcquisitionData();
Expand All @@ -78,5 +88,6 @@ export function getInitialState(): CommonState {
settings,
acquisitionData,
selectedAmountsVariant,
landingPageSettings,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ const router = () => {
path={`/${countryId}/contribute/:campaignCode?`}
element={
inThreeTier ? (
<ThreeTierLanding geoId={countryId} />
<ThreeTierLanding
geoId={countryId}
settings={store.getState().common.landingPageSettings}
/>
) : (
<SupporterPlusInitialLandingPage
thankYouRoute={thankYouRoute}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { getPromotion } from 'helpers/productPrice/promotions';
import * as storage from 'helpers/storage/storage';
import type { GeoId } from 'pages/geoIdConfig';
import { getGeoIdConfig } from 'pages/geoIdConfig';
import type { LandingPageSelection } from '../../../helpers/abTests/landingPageAbTests';
import Countdown from '../components/countdown';
import { LandingPageBanners } from '../components/landingPageBanners';
import { OneOffCard } from '../components/oneOffCard';
Expand Down Expand Up @@ -265,9 +266,11 @@ function getPlanCost(

type ThreeTierLandingProps = {
geoId: GeoId;
settings: LandingPageSelection;
};
export function ThreeTierLanding({
geoId,
settings,
}: ThreeTierLandingProps): JSX.Element {
const urlSearchParams = new URLSearchParams(window.location.search);
const urlSearchParamsProduct = urlSearchParams.get('product');
Expand Down Expand Up @@ -344,6 +347,7 @@ export function ThreeTierLanding({
}
}, [memoizedCurrentCountdownCampaign]);

// eslint-disable-next-line @typescript-eslint/no-unused-vars -- todo
const getHeadline = (
showCountdown: boolean,
currentCountdownSettings?: CountdownSetting,
Expand Down Expand Up @@ -553,20 +557,22 @@ export function ThreeTierLanding({
/>
)}
<h1 css={heading}>
{getHeadline(
showCountdown,
currentCountdownSettings,
campaignSettings,
)}
{/*{getHeadline(*/}
{/* showCountdown,*/}
{/* currentCountdownSettings,*/}
{/* campaignSettings,*/}
{/*)}*/}
{settings.copy.heading}
</h1>
<p css={standFirst}>
{campaignSettings?.copy.subheading ?? (
<>
We're not owned by a billionaire or shareholders - our readers
support us. Choose to join with one of the options below.{' '}
<strong>Cancel anytime.</strong>
</>
)}
{/*{campaignSettings?.copy.subheading ?? (*/}
{/* <>*/}
{/* We're not owned by a billionaire or shareholders - our readers*/}
{/* support us. Choose to join with one of the options below.{' '}*/}
{/* <strong>Cancel anytime.</strong>*/}
{/* </>*/}
{/*)}*/}
{settings.copy.subheading}
</p>
{campaignSettings?.tickerSettings && (
<TickerContainer tickerSettings={campaignSettings.tickerSettings} />
Expand Down
Loading