diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx
index af16bdea636..bcd6a0a4695 100644
--- a/client/src/components/Donation/donation-modal.tsx
+++ b/client/src/components/Donation/donation-modal.tsx
@@ -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');
diff --git a/client/src/components/Intro/learn-alert.tsx b/client/src/components/Intro/learn-alert.tsx
index 218b7997803..eb30a58b6ef 100644
--- a/client/src/components/Intro/learn-alert.tsx
+++ b/client/src/components/Intro/learn-alert.tsx
@@ -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 = (
-
-
- Launching Oct 19: freeCodeCamp is teaming up with researchers
- from Stanford and UPenn to study how to help people build strong coding
- habits.
-
-
- Would you like to get involved? You’ll get free coaching from our
- scientists.
-
-
-
- Learn about HabitLab
-
-
-
- );
-
const seasonalMessageAlert = (
@@ -63,30 +37,7 @@ const LearnAlert = ({
);
- const universityCreationAlert = (
-
-
- {t('learn.building-a-university')}
-
- {t('learn.if-help-university')}
-
-
-
- {t('donate.become-supporter')}
-
-
-
- );
-
- if (researchRecruitment.on) return researchRecruitmentAlert;
- if (universityCreation.on && !isDonating) return universityCreationAlert;
- if (seasonalMessage.on) return seasonalMessageAlert;
+ if (seasonalMessage.on && !isDonating) return seasonalMessageAlert;
return null;
};
diff --git a/client/src/components/growth-book/growth-book-wrapper.tsx b/client/src/components/growth-book/growth-book-wrapper.tsx
index f1e0b67dc61..2fe9a5daac6 100644
--- a/client/src/components/growth-book/growth-book-wrapper.tsx
+++ b/client/src/components/growth-book/growth-book-wrapper.tsx
@@ -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 (
diff --git a/client/src/utils/growthbook-cookie.test.ts b/client/src/utils/growthbook-cookie.test.ts
new file mode 100644
index 00000000000..d65f79027dd
--- /dev/null
+++ b/client/src/utils/growthbook-cookie.test.ts
@@ -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);
+ });
+});
diff --git a/client/src/utils/growthbook-cookie.ts b/client/src/utils/growthbook-cookie.ts
new file mode 100644
index 00000000000..66773a6aa29
--- /dev/null
+++ b/client/src/utils/growthbook-cookie.ts
@@ -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;
+};