Files
freeCodeCamp/client/src/components/growth-book/growth-book-wrapper.tsx
T
2025-07-28 18:25:14 +05:30

122 lines
3.2 KiB
TypeScript

import React, { useEffect, useMemo } from 'react';
import {
FeatureDefinition,
GrowthBook,
GrowthBookProvider
} from '@growthbook/growthbook-react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { userSelector, userFetchStateSelector } from '../../redux/selectors';
import envData from '../../../config/env.json';
import defaultGrowthBookFeatures from '../../../config/growthbook-features-default.json';
import type { User, UserFetchState } from '../../redux/prop-types';
import { getUUID } from '../../utils/growthbook-cookie';
import callGA from '../../analytics/call-ga';
import GrowthBookReduxConnector from './growth-book-redux-connector';
const { clientLocale, growthbookUri } = envData as {
clientLocale: string;
growthbookUri: string | null;
};
declare global {
interface Window {
dataLayer: [Record<string, number | string>];
}
}
const mapStateToProps = createSelector(
userSelector,
userFetchStateSelector,
(user: User | null, userFetchState: UserFetchState) => ({
user,
userFetchState
})
);
type StateProps = ReturnType<typeof mapStateToProps>;
interface GrowthBookWrapper extends StateProps {
children: JSX.Element;
}
interface UserAttributes {
id: string;
clientLocal: string;
staff?: boolean;
joinDateUnix?: number;
completedChallengesLength?: number;
signedIn?: true;
}
const GrowthBookWrapper = ({
children,
user,
userFetchState
}: GrowthBookWrapper) => {
const growthbook = useMemo(
() =>
new GrowthBook({
trackingCallback: (experiment, result) => {
callGA({
event: 'experiment_viewed',
event_category: 'experiment',
experiment_id: experiment.key,
variation_id: result.variationId
});
}
}),
[]
);
useEffect(() => {
async function setGrowthBookFeatures() {
if (!growthbookUri) {
// Defaults are added to facilitate testing, and avoid passing the related env
growthbook.setFeatures(defaultGrowthBookFeatures);
} else {
try {
const res = await fetch(growthbookUri);
const data = (await res.json()) as {
features: Record<string, FeatureDefinition>;
};
growthbook.setFeatures(data.features);
} catch (e) {
// TODO: report to sentry when it's enabled
console.error(e);
}
}
}
void setGrowthBookFeatures();
}, [growthbook]);
useEffect(() => {
if (userFetchState.complete) {
let userAttributes: UserAttributes = {
id: getUUID() as string,
clientLocal: clientLocale
};
if (user) {
userAttributes = {
...userAttributes,
staff: user.email.includes('@freecodecamp'),
joinDateUnix: Date.parse(user.joinDate),
completedChallengesLength: user.completedChallenges.length,
signedIn: true
};
}
growthbook.setAttributes(userAttributes);
}
}, [user, userFetchState, growthbook]);
return (
<GrowthBookProvider growthbook={growthbook}>
<GrowthBookReduxConnector>{children}</GrowthBookReduxConnector>
</GrowthBookProvider>
);
};
export default connect(mapStateToProps)(GrowthBookWrapper);