From 32af12437581e261b940872cf2e146b13220e11f Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 16:46:29 +0200 Subject: [PATCH 1/8] feat: signup corporate-only experiment with exposure and new_signup events --- frontend/common/stores/account-store.js | 10 ++++ frontend/common/stores/config-store.js | 9 ++++ frontend/package-lock.json | 8 ++-- frontend/package.json | 2 +- frontend/web/components/pages/HomePage.tsx | 54 +++++++++++++++++++--- 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index 2deb58f93073..bbf43b58dc77 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -17,6 +17,8 @@ import { hidePylon, identifyChatUser } from 'common/loadChat' import { service } from 'common/service' import { getBuildVersion } from 'common/services/useBuildVersion' import { createOnboardingSupportOptIn } from 'common/services/useOnboardingSupportOptIn' +import flagsmith from '@flagsmith/flagsmith' +import isFreeEmailDomain from 'common/utils/isFreeEmailDomain' const controller = { acceptInvite: (id) => { @@ -265,6 +267,14 @@ const controller = { .then(async (res) => { data.setToken(Project.cookieAuthEnabled ? 'true' : res.key) API.trackEvent(Constants.events.REGISTER) + flagsmith.trackEvent('new_signup', { + metadata: { + free_email_domain: isFreeEmailDomain(user.email), + invite: !!API.getInvite(), + signup_method: 'email', + utm_source: user.utm_data?.utm_source, + }, + }) if (API.getReferrer()) { API.trackEvent( Constants.events.REFERRER_REGISTERED(API.getReferrer().utm_source), diff --git a/frontend/common/stores/config-store.js b/frontend/common/stores/config-store.js index 8d7d7353a390..d1caca3e51fe 100644 --- a/frontend/common/stores/config-store.js +++ b/frontend/common/stores/config-store.js @@ -45,6 +45,15 @@ flagsmith environmentID: Project.flagsmith, onChange: controller.loaded, realtime: window.E2E ? false : Project.flagsmithRealtime, + ...(!window.E2E && + Project.flagsmithAnalytics && + Project.flagsmithClientEventsAPI + ? { + eventProcessorConfig: { + eventsApiUrl: Project.flagsmithClientEventsAPI, + }, + } + : {}), ...(Project.evaluationAnalyticsServerUrl ? { evaluationAnalyticsConfig: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4577b744f36e..2906ede9d66a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,7 @@ "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/helpers": "^0.3.2", "@dnd-kit/react": "^0.3.2", - "@flagsmith/flagsmith": "11.0.0-internal.11", + "@flagsmith/flagsmith": "12.1.0", "@ionic/react": "^7.5.3", "@react-oauth/google": "^0.2.8", "@reduxjs/toolkit": "1.9.1", @@ -3234,9 +3234,9 @@ } }, "node_modules/@flagsmith/flagsmith": { - "version": "11.0.0-internal.11", - "resolved": "https://registry.npmjs.org/@flagsmith/flagsmith/-/flagsmith-11.0.0-internal.11.tgz", - "integrity": "sha512-u2Mprn3GLz53TUEo1rpfK/18BMQbwf9Z5mKhs9jnUnRDjqh23Ud+83AwSPNjif5EjgXbrEg4p76gY8k/E/1RoA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@flagsmith/flagsmith/-/flagsmith-12.1.0.tgz", + "integrity": "sha512-xcT1J4gHWLYYXv+0jUNVSFM3ZPtKPdSe6n0ruAjwbqaGmTXL+B6W66b46J78SLJcCCgOlAuoYNYgGa/trPCBdw==", "license": "BSD-3-Clause" }, "node_modules/@floating-ui/core": { diff --git a/frontend/package.json b/frontend/package.json index 419e9ff0fc6f..e88a766e2eaf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,7 +63,7 @@ "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/helpers": "^0.3.2", "@dnd-kit/react": "^0.3.2", - "@flagsmith/flagsmith": "11.0.0-internal.11", + "@flagsmith/flagsmith": "12.1.0", "@ionic/react": "^7.5.3", "@react-oauth/google": "^0.2.8", "@reduxjs/toolkit": "1.9.1", diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index debcf25d64f3..e25114d88f9e 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -31,6 +31,21 @@ import AccountStore from 'common/stores/account-store' import { LoginRequest, RegisterRequest } from 'common/types/requests' import { useGetBuildVersionQuery } from 'common/services/useBuildVersion' import { useUTMs } from 'common/useUTMs' +import flagsmith from '@flagsmith/flagsmith' +import { storageGet, storageSet } from 'common/safeLocalStorage' + +const SIGNUP_CORPORATE_ONLY_FLAG = 'signup_corporate_only' +const SIGNUP_CORPORATE_ONLY_VARIANT = 'signup_corporate_only' +const SIGNUP_ANONYMOUS_ID_KEY = 'signup_anonymous_id' + +const recordSignupExperimentExposure = () => { + const id = storageGet(SIGNUP_ANONYMOUS_ID_KEY) || crypto.randomUUID() + storageSet(SIGNUP_ANONYMOUS_ID_KEY, id) + // transient is missing from the SDK's identify type + ;(flagsmith.identify as any)(id, {}, true).then(() => + flagsmith.getExperimentFlag(SIGNUP_CORPORATE_ONLY_FLAG), + ) +} const HomePage: React.FC = () => { const history = useHistory() @@ -180,6 +195,26 @@ const HomePage: React.FC = () => { currentLocation.indexOf('signup') !== -1) const disableSignup = preventSignup && isSignup const preventEmailPassword = Project.preventEmailPassword + + useEffect(() => { + if ( + isSignup && + !isInvite && + !preventEmailPassword && + !AccountStore.getUser() + ) { + recordSignupExperimentExposure() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSignup, isInvite, preventEmailPassword]) + + const signupFlag = flagsmith.getAllFlags()?.[SIGNUP_CORPORATE_ONLY_FLAG] + const blockGenericEmailDomain = + isSignup && + !isInvite && + !!signupFlag?.enabled && + signupFlag.variant === SIGNUP_CORPORATE_ONLY_VARIANT && + isFreeEmailDomain(email) const disableForgotPassword = Project.preventForgotPassword const oauths: React.ReactNode[] = [] const disableOauthRegister = Utils.getFlagsmithHasFeature( @@ -627,12 +662,16 @@ const HomePage: React.FC = () => { name='email' id='email' /> - {isFreeEmailDomain(email) && ( - - Signing up with a work email makes it easier - for co-workers to join your Flagsmith - organisation. - + {blockGenericEmailDomain ? ( + + ) : ( + isFreeEmailDomain(email) && ( + + Signing up with a work email makes it + easier for co-workers to join your + Flagsmith organisation. + + ) )} { disabled={ isLoading || isSaving || - !allRequirementsMet + !allRequirementsMet || + blockGenericEmailDomain } className='px-4 mt-3 full-width' type='submit' From ed57a056923a802235c4a215c648107c57254c33 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 17:15:42 +0200 Subject: [PATCH 2/8] refactor: simplify signup experiment to explicit exposure tracking --- frontend/common/utils/utils.tsx | 5 ++++ frontend/web/components/pages/HomePage.tsx | 28 ++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index f987451a981c..92f25c21760a 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -382,6 +382,11 @@ const Utils = Object.assign({}, BaseUtils, { getFlagsmithValue(key: string) { return flagsmith.getValue(key) }, + getFlagsmithVariant(key: string) { + return flagsmith.hasFeature(key) + ? flagsmith.getAllFlags()[key]?.variant + : undefined + }, getIdentitiesEndpoint(_project: ProjectType) { const project = _project || ProjectStore.model diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index e25114d88f9e..f9086f937306 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -34,17 +34,15 @@ import { useUTMs } from 'common/useUTMs' import flagsmith from '@flagsmith/flagsmith' import { storageGet, storageSet } from 'common/safeLocalStorage' -const SIGNUP_CORPORATE_ONLY_FLAG = 'signup_corporate_only' -const SIGNUP_CORPORATE_ONLY_VARIANT = 'signup_corporate_only' -const SIGNUP_ANONYMOUS_ID_KEY = 'signup_anonymous_id' - -const recordSignupExperimentExposure = () => { - const id = storageGet(SIGNUP_ANONYMOUS_ID_KEY) || crypto.randomUUID() - storageSet(SIGNUP_ANONYMOUS_ID_KEY, id) - // transient is missing from the SDK's identify type - ;(flagsmith.identify as any)(id, {}, true).then(() => - flagsmith.getExperimentFlag(SIGNUP_CORPORATE_ONLY_FLAG), - ) +const identifySignupVisitor = async () => { + const id = storageGet('signup_anonymous_id') || crypto.randomUUID() + storageSet('signup_anonymous_id', id) + // @ts-expect-error transient is missing from the SDK's identify type + await flagsmith.identify(id, {}, true) + const variant = Utils.getFlagsmithVariant('signup_corporate_only') + if (variant) { + flagsmith.trackExposureEvent('signup_corporate_only', { value: variant }) + } } const HomePage: React.FC = () => { @@ -203,17 +201,15 @@ const HomePage: React.FC = () => { !preventEmailPassword && !AccountStore.getUser() ) { - recordSignupExperimentExposure() + identifySignupVisitor() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSignup, isInvite, preventEmailPassword]) - const signupFlag = flagsmith.getAllFlags()?.[SIGNUP_CORPORATE_ONLY_FLAG] const blockGenericEmailDomain = - isSignup && !isInvite && - !!signupFlag?.enabled && - signupFlag.variant === SIGNUP_CORPORATE_ONLY_VARIANT && + Utils.getFlagsmithVariant('signup_corporate_only') === + 'signup_corporate_only' && isFreeEmailDomain(email) const disableForgotPassword = Project.preventForgotPassword const oauths: React.ReactNode[] = [] From d0147c8fbc46dbb026787001c5c5f341d143e59b Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 17:34:44 +0200 Subject: [PATCH 3/8] refactor: move signup experiment logic into useSignupExperiment hook --- frontend/common/useSignupExperiment.ts | 24 +++++++++++++++ frontend/common/utils/utils.tsx | 5 ---- frontend/web/components/pages/HomePage.tsx | 34 ++++------------------ 3 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 frontend/common/useSignupExperiment.ts diff --git a/frontend/common/useSignupExperiment.ts b/frontend/common/useSignupExperiment.ts new file mode 100644 index 000000000000..a25fa15b7b56 --- /dev/null +++ b/frontend/common/useSignupExperiment.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' +import flagsmith from '@flagsmith/flagsmith' +import { storageGet, storageSet } from './safeLocalStorage' + +export default function useSignupExperiment(useEnvironmentFlag: boolean) { + const [variant, setVariant] = useState() + + useEffect(() => { + if (useEnvironmentFlag) { + return + } + const identifyAndExpose = async () => { + const id = storageGet('signup_anonymous_id') || crypto.randomUUID() + storageSet('signup_anonymous_id', id) + // @ts-expect-error transient is missing from the SDK's identify type + await flagsmith.identify(id, {}, true) + const flag = flagsmith.getExperimentFlag('signup_corporate_only') + setVariant(flag?.enabled ? flag.variant : undefined) + } + identifyAndExpose() + }, [useEnvironmentFlag]) + + return variant +} diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 92f25c21760a..f987451a981c 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -382,11 +382,6 @@ const Utils = Object.assign({}, BaseUtils, { getFlagsmithValue(key: string) { return flagsmith.getValue(key) }, - getFlagsmithVariant(key: string) { - return flagsmith.hasFeature(key) - ? flagsmith.getAllFlags()[key]?.variant - : undefined - }, getIdentitiesEndpoint(_project: ProjectType) { const project = _project || ProjectStore.model diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index f9086f937306..8d9f382ef3b5 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -31,19 +31,7 @@ import AccountStore from 'common/stores/account-store' import { LoginRequest, RegisterRequest } from 'common/types/requests' import { useGetBuildVersionQuery } from 'common/services/useBuildVersion' import { useUTMs } from 'common/useUTMs' -import flagsmith from '@flagsmith/flagsmith' -import { storageGet, storageSet } from 'common/safeLocalStorage' - -const identifySignupVisitor = async () => { - const id = storageGet('signup_anonymous_id') || crypto.randomUUID() - storageSet('signup_anonymous_id', id) - // @ts-expect-error transient is missing from the SDK's identify type - await flagsmith.identify(id, {}, true) - const variant = Utils.getFlagsmithVariant('signup_corporate_only') - if (variant) { - flagsmith.trackExposureEvent('signup_corporate_only', { value: variant }) - } -} +import useSignupExperiment from 'common/useSignupExperiment' const HomePage: React.FC = () => { const history = useHistory() @@ -194,23 +182,11 @@ const HomePage: React.FC = () => { const disableSignup = preventSignup && isSignup const preventEmailPassword = Project.preventEmailPassword - useEffect(() => { - if ( - isSignup && - !isInvite && - !preventEmailPassword && - !AccountStore.getUser() - ) { - identifySignupVisitor() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSignup, isInvite, preventEmailPassword]) - + const signupVariant = useSignupExperiment( + !isSignup || isInvite || !!preventEmailPassword || !!AccountStore.getUser(), + ) const blockGenericEmailDomain = - !isInvite && - Utils.getFlagsmithVariant('signup_corporate_only') === - 'signup_corporate_only' && - isFreeEmailDomain(email) + signupVariant === 'signup_corporate_only' && isFreeEmailDomain(email) const disableForgotPassword = Project.preventForgotPassword const oauths: React.ReactNode[] = [] const disableOauthRegister = Utils.getFlagsmithHasFeature( From 9af5723fd1776fdb37a420b8ff7ef7747427d599 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 17:38:10 +0200 Subject: [PATCH 4/8] fix: require first and last name before enabling signup --- frontend/web/components/pages/HomePage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index 8d9f382ef3b5..55becef5684e 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -675,6 +675,8 @@ const HomePage: React.FC = () => { isLoading || isSaving || !allRequirementsMet || + !firstName.trim() || + !lastName.trim() || blockGenericEmailDomain } className='px-4 mt-3 full-width' From f855915a97e988c6efdf75ff62fa28175ed82fc0 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 18:14:12 +0200 Subject: [PATCH 5/8] fix: use default events API for Flagsmith-on-Flagsmith events --- frontend/common/stores/config-store.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/frontend/common/stores/config-store.js b/frontend/common/stores/config-store.js index d1caca3e51fe..8d7d7353a390 100644 --- a/frontend/common/stores/config-store.js +++ b/frontend/common/stores/config-store.js @@ -45,15 +45,6 @@ flagsmith environmentID: Project.flagsmith, onChange: controller.loaded, realtime: window.E2E ? false : Project.flagsmithRealtime, - ...(!window.E2E && - Project.flagsmithAnalytics && - Project.flagsmithClientEventsAPI - ? { - eventProcessorConfig: { - eventsApiUrl: Project.flagsmithClientEventsAPI, - }, - } - : {}), ...(Project.evaluationAnalyticsServerUrl ? { evaluationAnalyticsConfig: { From 56c672eeb319e66a15457c28d7976ab07899dbf8 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 9 Jul 2026 18:15:57 +0200 Subject: [PATCH 6/8] chore: show signup experiment variant for debugging --- frontend/web/components/pages/HomePage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index 55becef5684e..e03c0931e9ab 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -581,6 +581,10 @@ const HomePage: React.FC = () => { )}
+ {/* TODO: remove experiment debug output */} +
+ variant: {signupVariant || 'control'} +
Date: Thu, 9 Jul 2026 18:17:18 +0200 Subject: [PATCH 7/8] Revert "chore: show signup experiment variant for debugging" This reverts commit 56c672eeb319e66a15457c28d7976ab07899dbf8. --- frontend/web/components/pages/HomePage.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index e03c0931e9ab..55becef5684e 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -581,10 +581,6 @@ const HomePage: React.FC = () => { )}
- {/* TODO: remove experiment debug output */} -
- variant: {signupVariant || 'control'} -
Date: Fri, 10 Jul 2026 13:08:58 +0200 Subject: [PATCH 8/8] feat: include email domain in new_signup metadata for personal emails --- frontend/common/stores/account-store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index bbf43b58dc77..60bf4ea3c7f6 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -267,9 +267,11 @@ const controller = { .then(async (res) => { data.setToken(Project.cookieAuthEnabled ? 'true' : res.key) API.trackEvent(Constants.events.REGISTER) + const freeEmailDomain = isFreeEmailDomain(user.email) flagsmith.trackEvent('new_signup', { metadata: { - free_email_domain: isFreeEmailDomain(user.email), + ...(freeEmailDomain && { domain: user.email.split('@')[1] }), + free_email_domain: freeEmailDomain, invite: !!API.getInvite(), signup_method: 'email', utm_source: user.utm_data?.utm_source,