mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): reimplement Growthbook (#52316)
This commit is contained in:
@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
|
||||
import { goToAnchor } from 'react-scrollable-anchor';
|
||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useFeature } from '@growthbook/growthbook-react';
|
||||
|
||||
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
|
||||
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
|
||||
@@ -125,6 +126,8 @@ function DonateModal({
|
||||
setDonationAttempted(true);
|
||||
};
|
||||
|
||||
useFeature('aa-test-in-component');
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
void playTone('donation');
|
||||
|
||||
@@ -14,34 +14,8 @@ const LearnAlert = ({
|
||||
isDonating
|
||||
}: LearnAlertProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const researchRecruitment = useFeature('show-research-recruitment-alert');
|
||||
const universityCreation = useFeature('university-creation-alert');
|
||||
const seasonalMessage = useFeature('seasonal-alert');
|
||||
|
||||
const researchRecruitmentAlert = (
|
||||
<Alert variant='info' className='annual-donation-alert'>
|
||||
<p>
|
||||
<b>Launching Oct 19</b>: freeCodeCamp is teaming up with researchers
|
||||
from Stanford and UPenn to study how to help people build strong coding
|
||||
habits.
|
||||
</p>
|
||||
<p style={{ marginBottom: 20, marginTop: 14 }}>
|
||||
Would you like to get involved? You’ll get free coaching from our
|
||||
scientists.
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Link
|
||||
className='btn'
|
||||
key='donate'
|
||||
sameTab={false}
|
||||
to='https://wharton.qualtrics.com/jfe/form/SV_57rJfXROkQDDU2y'
|
||||
>
|
||||
Learn about HabitLab
|
||||
</Link>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const seasonalMessageAlert = (
|
||||
<Alert variant='info' className='annual-donation-alert'>
|
||||
<p>
|
||||
@@ -63,30 +37,7 @@ const LearnAlert = ({
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const universityCreationAlert = (
|
||||
<Alert variant='info' className='annual-donation-alert'>
|
||||
<p>
|
||||
<b>{t('learn.building-a-university')}</b>
|
||||
</p>
|
||||
<p>{t('learn.if-help-university')}</p>
|
||||
<hr />
|
||||
<p className={'text-center'}>
|
||||
<Link
|
||||
className='btn'
|
||||
key='donate'
|
||||
sameTab={false}
|
||||
to='/donate'
|
||||
onClick={onDonationAlertClick}
|
||||
>
|
||||
{t('donate.become-supporter')}
|
||||
</Link>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
if (researchRecruitment.on) return researchRecruitmentAlert;
|
||||
if (universityCreation.on && !isDonating) return universityCreationAlert;
|
||||
if (seasonalMessage.on) return seasonalMessageAlert;
|
||||
if (seasonalMessage.on && !isDonating) return seasonalMessageAlert;
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import sha1 from 'sha-1';
|
||||
import {
|
||||
FeatureDefinition,
|
||||
GrowthBook,
|
||||
@@ -7,9 +6,14 @@ import {
|
||||
} from '@growthbook/growthbook-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { isSignedInSelector, userSelector } from '../../redux/selectors';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
userSelector,
|
||||
userFetchStateSelector
|
||||
} from '../../redux/selectors';
|
||||
import envData from '../../../config/env.json';
|
||||
import { User } from '../../redux/prop-types';
|
||||
import { User, UserFetchState } from '../../redux/prop-types';
|
||||
import { getUUID } from '../../utils/growthbook-cookie';
|
||||
import GrowthBookReduxConnector from './growth-book-redux-connector';
|
||||
|
||||
const { clientLocale, growthbookUri } = envData as {
|
||||
@@ -37,9 +41,11 @@ const growthbook = new GrowthBook({
|
||||
const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
userSelector,
|
||||
(isSignedIn, user: User) => ({
|
||||
userFetchStateSelector,
|
||||
(isSignedIn, user: User, userFetchState: UserFetchState) => ({
|
||||
isSignedIn,
|
||||
user
|
||||
user,
|
||||
userFetchState
|
||||
})
|
||||
);
|
||||
|
||||
@@ -48,10 +54,20 @@ interface GrowthBookWrapper extends StateProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface UserAttributes {
|
||||
id: string;
|
||||
clientLocal: string;
|
||||
staff?: boolean;
|
||||
joinDateUnix?: number;
|
||||
completedChallengesLength?: number;
|
||||
signedIn?: true;
|
||||
}
|
||||
|
||||
const GrowthBookWrapper = ({
|
||||
children,
|
||||
isSignedIn,
|
||||
user
|
||||
user,
|
||||
userFetchState
|
||||
}: GrowthBookWrapper) => {
|
||||
useEffect(() => {
|
||||
async function setGrowthBookFeatures() {
|
||||
@@ -73,16 +89,24 @@ const GrowthBookWrapper = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
growthbook.setAttributes({
|
||||
id: sha1(user.email),
|
||||
staff: user.email.includes('@freecodecamp'),
|
||||
clientLocal: clientLocale,
|
||||
joinDateUnix: Date.parse(user.joinDate),
|
||||
completedChallengesLength: user.completedChallenges.length
|
||||
});
|
||||
if (userFetchState.complete) {
|
||||
let userAttributes: UserAttributes = {
|
||||
id: getUUID() as string,
|
||||
clientLocal: clientLocale
|
||||
};
|
||||
|
||||
if (isSignedIn) {
|
||||
userAttributes = {
|
||||
...userAttributes,
|
||||
staff: user.email.includes('@freecodecamp'),
|
||||
joinDateUnix: Date.parse(user.joinDate),
|
||||
completedChallengesLength: user.completedChallenges.length,
|
||||
signedIn: true
|
||||
};
|
||||
}
|
||||
growthbook.setAttributes(userAttributes);
|
||||
}
|
||||
}, [isSignedIn, user.email, user.joinDate, user.completedChallenges]);
|
||||
}, [isSignedIn, user, userFetchState]);
|
||||
|
||||
return (
|
||||
<GrowthBookProvider growthbook={growthbook}>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getUUID } from './growthbook-cookie';
|
||||
|
||||
describe('getUUID', () => {
|
||||
let originalCookie: string;
|
||||
|
||||
beforeEach(() => {
|
||||
global.crypto = {
|
||||
...global.crypto,
|
||||
randomUUID: () => '123e4567-e89b-12d3-a456-426614174000'
|
||||
};
|
||||
|
||||
// Save original cookie
|
||||
originalCookie = document.cookie;
|
||||
|
||||
// Clear the cookie before each test
|
||||
document.cookie.split(';').forEach(c => {
|
||||
document.cookie = c
|
||||
.replace(/^ +/, '')
|
||||
.replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original cookie
|
||||
document.cookie = originalCookie;
|
||||
});
|
||||
|
||||
it('should generate a new UUID if none exists', () => {
|
||||
const uuid = getUUID();
|
||||
expect(uuid).toBeDefined();
|
||||
expect(document.cookie).toContain('gbuuid=' + uuid);
|
||||
});
|
||||
|
||||
it('should return the existing UUID if one exists', () => {
|
||||
const existingUUID = '123e4567-e89b-12d3-a456-426614174000';
|
||||
document.cookie = 'gbuuid=' + existingUUID;
|
||||
const uuid = getUUID();
|
||||
expect(uuid).toBe(existingUUID);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
export const getUUID = () => {
|
||||
const COOKIE_NAME = 'gbuuid';
|
||||
const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
|
||||
|
||||
// use the browsers crypto.randomUUID if set
|
||||
const genUUID = () => {
|
||||
if (window?.crypto?.randomUUID) return window.crypto.randomUUID();
|
||||
|
||||
// return a random UUID style string`
|
||||
return ((1e7).toString() + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
c =>
|
||||
(
|
||||
Number(c) ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] &
|
||||
(15 >> (Number(c) / 4)))
|
||||
).toString(16)
|
||||
);
|
||||
};
|
||||
|
||||
const getCookie = (name: string) => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
||||
};
|
||||
const setCookie = (name: string, value: string) => {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
|
||||
document.cookie = name + '=' + value + ';path=/;expires=' + d.toUTCString();
|
||||
};
|
||||
|
||||
// get the existing UUID from cookie if set, otherwise create one and store it in the cookie
|
||||
if (getCookie(COOKIE_NAME)) return getCookie(COOKIE_NAME);
|
||||
|
||||
const uuid = genUUID();
|
||||
setCookie(COOKIE_NAME, uuid);
|
||||
return uuid;
|
||||
};
|
||||
Reference in New Issue
Block a user