Skip to content
Open
12 changes: 12 additions & 0 deletions frontend/common/stores/account-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -265,6 +267,16 @@ 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: {
...(freeEmailDomain && { domain: user.email.split('@')[1] }),
free_email_domain: freeEmailDomain,
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),
Expand Down
24 changes: 24 additions & 0 deletions frontend/common/useSignupExperiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'
import flagsmith from '@flagsmith/flagsmith'
import { storageGet, storageSet } from './safeLocalStorage'

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Relative import violates path-import guideline.

./safeLocalStorage is a relative import; per repository guidelines this file should import via the common/ path instead.

♻️ Proposed fix
-import { storageGet, storageSet } from './safeLocalStorage'
+import { storageGet, storageSet } from 'common/safeLocalStorage'

As per coding guidelines, "Use only common/, components/, and project/ import paths; do not use relative imports" for frontend/**/*.{js,jsx,ts,tsx}.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { storageGet, storageSet } from './safeLocalStorage'
import { storageGet, storageSet } from 'common/safeLocalStorage'

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is enforced by the repo's linter: @dword-design/import-alias/prefer-alias rewrites imports to the common/ alias except for same-directory imports, which it keeps relative — eslint --fix converts common/safeLocalStorage back to ./safeLocalStorage here. Keeping the linter's form.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "import-alias" frontend/.eslintrc* 2>/dev/null
fd -H '.eslintrc*' frontend -x cat {}

Length of output: 5028


Confirmed — the .eslintrc.js config enables @dword-design/import-alias/prefer-alias with common aliased to ./common/, and this plugin intentionally keeps same-directory imports relative rather than rewriting them to the alias. Since useSignupExperiment.ts and safeLocalStorage both live in frontend/common/, the relative import here is the linter's expected/enforced form, not a violation.

@Zaimwa9 thanks for the clarification — that makes sense, and my original suggestion would have been immediately reverted by eslint --fix. Withdrawing the comment.

(=^・ω・^=) ah, makes sense now


export default function useSignupExperiment(useEnvironmentFlag: boolean) {
const [variant, setVariant] = useState<string>()

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])
Comment on lines +8 to +21

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files 'frontend/common/*' | rg 'useSignupExperiment|HomePage|signup|Experiment'

Repository: Flagsmith/flagsmith

Length of output: 406


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files 'frontend/common/*' | rg 'useSignupExperiment|HomePage|signup|Experiment'

Repository: Flagsmith/flagsmith

Length of output: 413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Search usages:\n'
rg -n "useSignupExperiment" frontend/common || true

printf '\nHook file:\n'
sed -n '1,200p' frontend/common/useSignupExperiment.ts

printf '\nHomePage candidates:\n'
rg -n "blockGenericEmailDomain|signup_corporate_only|variant" frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 1970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
paths = [
    Path('frontend/common/useSignupExperiment.ts'),
]
for p in paths:
    print(f'--- {p} ---')
    if p.exists():
        for i, line in enumerate(p.read_text().splitlines(), 1):
            print(f'{i:4}: {line}')
    else:
        print('missing')
PY

Repository: Flagsmith/flagsmith

Length of output: 1185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'useSignupExperiment usages:\n'
rg -n "useSignupExperiment" frontend/common || true

printf '\nuseSignupExperiment.ts:\n'
cat -n frontend/common/useSignupExperiment.ts

printf '\nPotential consumers / related terms:\n'
rg -n "blockGenericEmailDomain|signup_corporate_only|variant" frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 2180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'frontend/**/*.ts' 'frontend/**/*.tsx' | rg 'useSignupExperiment|HomePage|Signup|signup' || true

Repository: Flagsmith/flagsmith

Length of output: 239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'HomePage outline:\n'
ast-grep outline frontend/web/components/pages/HomePage.tsx --view expanded || true

printf '\nHomePage relevant section:\n'
sed -n '1,260p' frontend/web/components/pages/HomePage.tsx

printf '\nSearch for signup email gating:\n'
rg -n "blockGenericEmailDomain|useSignupExperiment|signup_corporate_only|generic email|corporate" frontend/web/components/pages/HomePage.tsx frontend/common || true

Repository: Flagsmith/flagsmith

Length of output: 10025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'HomePage around email gating and submit:\n'
sed -n '560,760p' frontend/web/components/pages/HomePage.tsx

printf '\nSubmit handlers:\n'
rg -n "handle.*Submit|onSubmit|blockGenericEmailDomain|isFreeEmailDomain|signupVariant" frontend/web/components/pages/HomePage.tsx

Repository: Flagsmith/flagsmith

Length of output: 8780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Submit handler around first form:\n'
sed -n '340,430p' frontend/web/components/pages/HomePage.tsx

printf '\nSubmit handler around second form:\n'
sed -n '490,550p' frontend/web/components/pages/HomePage.tsx

Repository: Flagsmith/flagsmith

Length of output: 7473


Wait for the experiment flag before treating signup as ready. useSignupExperiment() returns undefined on the first render, and HomePage only enforces the restriction in render state. A fast submit can reach register() before flagsmith.identify() resolves, so the corporate-only gate and exposure never apply for that attempt. Add a ready/loading signal or re-check in the submit handler, and guard the async state update on unmount.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignoring

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zaimwa9, understood — I'll leave this as-is. Let me know if you'd like to revisit it later.

(๑•́ - •̀๑)


return variant
}
Comment on lines +5 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Local useState in a common/ hook may conflict with the Redux Toolkit state-management guideline.

This hook manages variant with plain React useState rather than Redux Toolkit/RTK Query as prescribed for files under frontend/common/**/*.{ts,tsx}. If this experiment variant needs to be consumed by more than one component, consider modelling it as store state instead of a bespoke hook-local state.

As per coding guidelines, "Use Redux Toolkit and RTK Query for state management, with the store located in common/store.ts" for frontend/common/**/*.{ts,tsx}.

Source: Coding guidelines

8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 21 additions & 7 deletions frontend/web/components/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +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 useSignupExperiment from 'common/useSignupExperiment'

const HomePage: React.FC = () => {
const history = useHistory()
Expand Down Expand Up @@ -180,6 +181,12 @@ const HomePage: React.FC = () => {
currentLocation.indexOf('signup') !== -1)
const disableSignup = preventSignup && isSignup
const preventEmailPassword = Project.preventEmailPassword

const signupVariant = useSignupExperiment(
!isSignup || isInvite || !!preventEmailPassword || !!AccountStore.getUser(),
)
const blockGenericEmailDomain =
signupVariant === 'signup_corporate_only' && isFreeEmailDomain(email)
Comment on lines +184 to +189

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated magic string 'signup_corporate_only' across files.

The variant name is hardcoded here and again in useSignupExperiment.ts (getExperimentFlag('signup_corporate_only')). Extracting a shared constant would prevent silent drift if the flag/variant name ever changes.

♻️ Suggested shared constant
// common/useSignupExperiment.ts
export const SIGNUP_CORPORATE_ONLY_VARIANT = 'signup_corporate_only'
-  const blockGenericEmailDomain =
-    signupVariant === 'signup_corporate_only' && isFreeEmailDomain(email)
+  const blockGenericEmailDomain =
+    signupVariant === SIGNUP_CORPORATE_ONLY_VARIANT && isFreeEmailDomain(email)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zaimwa9, understood — I'll leave it as is.

(◡﹏◡✿)

const disableForgotPassword = Project.preventForgotPassword
const oauths: React.ReactNode[] = []
const disableOauthRegister = Utils.getFlagsmithHasFeature(
Expand Down Expand Up @@ -627,12 +634,16 @@ const HomePage: React.FC = () => {
name='email'
id='email'
/>
{isFreeEmailDomain(email) && (
<InfoMessage>
Signing up with a work email makes it easier
for co-workers to join your Flagsmith
organisation.
</InfoMessage>
{blockGenericEmailDomain ? (
<ErrorMessage error='Please use your work email address to create your account.' />
) : (
isFreeEmailDomain(email) && (
<InfoMessage>
Signing up with a work email makes it
easier for co-workers to join your
Flagsmith organisation.
</InfoMessage>
)
)}
<InputGroup
title='Password'
Expand Down Expand Up @@ -663,7 +674,10 @@ const HomePage: React.FC = () => {
disabled={
isLoading ||
isSaving ||
!allRequirementsMet
!allRequirementsMet ||
!firstName.trim() ||
!lastName.trim() ||
blockGenericEmailDomain
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
className='px-4 mt-3 full-width'
type='submit'
Expand Down
Loading