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; +};