diff --git a/frontend/.claude/commands/api.md b/frontend/.claude/commands/api.md
new file mode 100644
index 000000000000..a786ca166a1a
--- /dev/null
+++ b/frontend/.claude/commands/api.md
@@ -0,0 +1,12 @@
+---
+description: Generate a new RTK Query API service
+---
+
+
+
+Generate a new API service. Follow these steps:
+
+1. Go through the process mentioned in `.claude/context/api-integration.md`
+2. If I haven't specified, attempt to find where I'd want to create this component in the frontend
+
+Context file: `.claude/context/api-integration.md`
diff --git a/frontend/.claude/commands/backend.md b/frontend/.claude/commands/backend.md
new file mode 100644
index 000000000000..547e3a1edf6c
--- /dev/null
+++ b/frontend/.claude/commands/backend.md
@@ -0,0 +1,14 @@
+---
+description: Search the backend codebase for endpoint details
+---
+
+Search the `../api` codebase for the requested endpoint.
+
+Look for:
+1. Route definitions (URL path)
+2. HTTP method (GET, POST, PUT, DELETE)
+3. Request validation schema (request body/params types)
+4. Response structure (what data is returned)
+5. Authentication/authorization requirements
+
+If the endpoint isn't found, check swagger docs: https://staging.flagsmith.com/api/v1/docs/
diff --git a/frontend/.claude/commands/check-staged.md b/frontend/.claude/commands/check-staged.md
new file mode 100644
index 000000000000..4da239a8b1b5
--- /dev/null
+++ b/frontend/.claude/commands/check-staged.md
@@ -0,0 +1,8 @@
+---
+description: Run type checking and linting on staged files
+---
+
+Run TypeScript checking and linting on all currently staged files, similar to pre-commit hooks. Steps:
+1. Run `npm run check:staged` to typecheck and lint only staged files
+2. Report any type errors or linting issues found
+3. If errors exist, offer to fix them
diff --git a/frontend/.claude/commands/check.md b/frontend/.claude/commands/check.md
new file mode 100644
index 000000000000..60aaded6a3c1
--- /dev/null
+++ b/frontend/.claude/commands/check.md
@@ -0,0 +1,9 @@
+---
+description: Run type checking and linting
+---
+
+Run the following checks on the codebase:
+
+1. `npx lint-staged --allow-empty` - Fix linting issues on staged files only (same as git hook)
+
+Report any errors found and offer to fix them.
diff --git a/frontend/.claude/commands/context.md b/frontend/.claude/commands/context.md
new file mode 100644
index 000000000000..c67309252944
--- /dev/null
+++ b/frontend/.claude/commands/context.md
@@ -0,0 +1,12 @@
+---
+description: Load detailed context files for specific topics
+---
+
+Available context files in `.claude/context/`:
+
+1. **api-integration.md** - API integration workflow, Redux setup, cross-platform patterns
+2. **architecture.md** - Environment config, tech stack, additional rules
+3. **feature-flags/** - Flagsmith feature flags (usage + MCP workflows)
+4. **patterns/** - Code patterns (API, mobile)
+
+Which context would you like to explore?
diff --git a/frontend/.claude/commands/feature-flag.md b/frontend/.claude/commands/feature-flag.md
new file mode 100644
index 000000000000..42372209279d
--- /dev/null
+++ b/frontend/.claude/commands/feature-flag.md
@@ -0,0 +1,5 @@
+---
+description: Create a feature flag
+---
+
+1. Create a feature flag using the context defined in `.claude/context/feature-flags/`
diff --git a/frontend/.claude/commands/form.md.draft b/frontend/.claude/commands/form.md.draft
new file mode 100644
index 000000000000..401f0d744d6f
--- /dev/null
+++ b/frontend/.claude/commands/form.md.draft
@@ -0,0 +1,15 @@
+---
+description: Create a new form using Yup + Formik pattern
+---
+
+Create a form following the standard pattern:
+
+1. Use `useFormik` hook with `validationSchema` from Yup
+2. Always include `validateOnMount: true`
+3. Use `validateForm` utility from `project/utils/forms/validateForm` in submit handler
+4. Use `InputGroup` component with `touched` and `error` props
+5. For special inputs (date, phone, select), use `component` prop on InputGroup
+
+Reference: `/examples/forms/ComprehensiveFormExample.tsx`
+
+Context file: `.claude/context/forms.md`
diff --git a/frontend/.claude/commands/ui-patterns.md b/frontend/.claude/commands/ui-patterns.md
new file mode 100644
index 000000000000..7abce1f34fa0
--- /dev/null
+++ b/frontend/.claude/commands/ui-patterns.md
@@ -0,0 +1,47 @@
+# UI Patterns & Best Practices
+
+## Confirmation Dialogs
+
+**NEVER use `window.confirm`** - Always use the `openConfirm` function from `components/base/Modal`.
+
+### Correct Usage
+
+```typescript
+import { openConfirm } from 'components/base/Modal'
+
+// Signature: openConfirm(title, body, onYes, onNo?, challenge?)
+openConfirm(
+ 'Delete Partner',
+ 'Are you sure you want to delete this partner?',
+ async (closeModal) => {
+ const res = await deleteAction()
+ if (!res.error) {
+ toast(null, 'Partner deleted successfully')
+ closeModal() // Always call closeModal to dismiss the dialog
+ }
+ },
+)
+```
+
+### Parameters
+- `title: string` - Dialog title
+- `body: ReactNode` - Dialog content (can be JSX)
+- `onYes: (closeModal: () => void) => void` - Callback when user confirms
+- `onNo?: () => void` - Optional callback when user cancels
+- `challenge?: string` - Optional challenge text user must type to confirm
+
+### Key Points
+- The `onYes` callback receives a `closeModal` function
+- Always call `closeModal()` when the action completes successfully
+- Can be async - use `async (closeModal) => { ... }`
+
+## Backend Integration
+
+### Always Run API Types Sync Before API Work
+
+When using `/api` to generate new API services, the command automatically runs `/api-types-sync` first to:
+1. Pull latest backend changes (`git pull` in `../api`)
+2. Sync frontend types with backend serializers
+3. Ensure types are up-to-date before generating new services
+
+This prevents type mismatches and ensures consistency.
diff --git a/frontend/.claude/context/api-integration.md b/frontend/.claude/context/api-integration.md
new file mode 100644
index 000000000000..3ba3fd95997b
--- /dev/null
+++ b/frontend/.claude/context/api-integration.md
@@ -0,0 +1,220 @@
+# API Integration Guide
+
+## Overview
+
+This project uses **RTK Query** (Redux Toolkit Query) for all API calls. The workflow is optimized for type safety and automatic sync with backend Django serializers.
+
+**Finding Backend Endpoints**: See `.claude/context/backend-integration.md` for strategies to locate and understand backend API endpoints.
+
+## Quick Start: Adding a New Endpoint (Complete Example)
+
+This example shows how to add a new endpoint for fetching company invoices (a real implementation from the codebase).
+
+### Step 1: Find Backend Endpoint
+
+Use strategies from `backend-integration.md` to locate the endpoint.
+
+**Backend endpoint found:** `GET /organisations/{organisation_id}/invoices`
+
+### Step 2: Add Request Type
+
+**File:** `common/types/requests.ts`
+
+```typescript
+export type Req = {
+ // ... existing types
+ getCompanyInvoices: {
+ organisation_id: string
+ }
+}
+```
+
+### Step 3: Add RTK Query Endpoint
+
+**File:** `common/services/useInvoice.ts`
+
+```typescript
+export const invoiceService = service
+ .enhanceEndpoints({ addTagTypes: ['Invoice'] })
+ .injectEndpoints({
+ endpoints: (builder) => ({
+ // Add new endpoint
+ getCompanyInvoices: builder.query<
+ Res['invoices'],
+ Req['getCompanyInvoices']
+ >({
+ providesTags: [{ id: 'LIST', type: 'Invoice' }],
+ query: (req) => ({
+ url: `organisations/${organisation_id}/invoices`,
+ }),
+ transformResponse(res: InvoiceSummary[]) {
+ return res?.map((v) => ({ ...v, date: v.date * 1000 }))
+ },
+ }),
+ }),
+ })
+
+export const {
+ useGetCompanyInvoicesQuery, // Export new hook
+ // END OF EXPORTS
+} = invoiceService
+```
+
+### Step 4: Use in Component
+
+```typescript
+import { useGetCompanyInvoicesQuery } from 'common/services/useInvoice'
+
+const MyComponent = () => {
+ const { subscriptionDetail } = useDefaultSubscription()
+ const companyId = subscriptionDetail?.company_id
+
+ const { data: invoices, error, isLoading } = useGetCompanyInvoicesQuery(
+ { organisation_id: `${companyId}` },
+ { skip: !organisation_id } // Skip if no company ID
+ )
+
+ if (isLoading) return
+ if (error) return {error}
+
+ return (
+
+ {invoices?.map(inv =>
{inv.id}
)}
+
+ )
+}
+```
+
+### Step 5: Run Linter
+
+```bash
+npx eslint --fix common/types/requests.ts common/services/useInvoice.ts
+```
+
+**Done!** The endpoint is now integrated and ready to use.
+
+## Manual Service Creation (Rare Cases)
+
+If you need to manually create a service (follow template in `.claude/commands/api.md`):
+
+1. **Identify backend endpoint**
+ - Use `/backend ` to search backend codebase
+ - Check `../api/apps/*/serializers.py`, `../api/apps/*/views.py`, `apps/*/urls.py`
+ - Or swagger docs: https://api.flagsmith.com/api/v1/docs/
+
+2. **Define types** in `common/types/`
+ - Response: `export type EntityName = { field: type }` in `responses.ts`
+ - Add to `Res` type: `entityName: EntityName`
+ - Request: `getEntityName: { param: type }` in `requests.ts`
+
+3. **Create service** `common/services/use{Entity}.ts`
+ - Use `builder.query` for GET, `builder.mutation` for POST/PUT/DELETE
+ - Configure cache tags: `providesTags`, `invalidatesTags`
+ - Export hooks: `useGetEntityQuery`, `useCreateEntityMutation`
+
+4. **Register in type map** `.claude/api-type-map.json`
+ - Add to `response_types` or `request_types` with full metadata
+ - Increment `_metadata.totalTypes`
+
+5. **Run linter**
+ ```bash
+ npx eslint --fix common/services/use*.ts common/types/*.ts
+ ```
+
+### Backend → Frontend Type Mapping
+
+| Django Type | TypeScript |
+|------------|------------|
+| `CharField` | `string` |
+| `IntegerField` | `number` |
+| `BooleanField` | `boolean` |
+| `DateTimeField` | `string` (ISO) |
+| `required=False` | `field?: type` |
+| `many=True` | `Type[]` |
+| Enum/Choices | `'A' \| 'B'` |
+
+### Example Service Structure
+
+```typescript
+import { service } from 'common/service'
+import { Req } from 'common/types/requests'
+import { Res } from 'common/types/responses'
+
+export const entityService = service
+ .enhanceEndpoints({ addTagTypes: ['Entity'] })
+ .injectEndpoints({
+ endpoints: (builder) => ({
+ getEntity: builder.query({
+ providesTags: (res) => [{ id: res?.id, type: 'Entity' }],
+ query: (query) => ({
+ url: `entities/${query.id}`,
+ }),
+ }),
+ updateEntity: builder.mutation({
+ invalidatesTags: (res) => [
+ { id: 'LIST', type: 'Entity' },
+ { id: res?.id, type: 'Entity' },
+ ],
+ query: (query) => ({
+ body: query,
+ method: 'PUT',
+ url: `entities/${query.id}`,
+ }),
+ }),
+ // END OF ENDPOINTS
+ }),
+ })
+
+export const {
+ useGetEntityQuery,
+ useUpdateEntityMutation,
+ // END OF EXPORTS
+} = entityService
+```
+
+See `common/services/useAuditLog.ts` for a complete example.
+
+## State Management
+
+- **Redux Toolkit + RTK Query** for all API calls
+- Store: `common/store.ts` with redux-persist
+- Base service: `common/service.ts`
+ The `npx ssg` CLI requires interactive input that cannot be automated. Instead, **manually create RTK Query services** following the patterns in existing service files.
+- **IMPORTANT**: When implementing API logic, prefer implementing it in the RTK Query service layer (using `transformResponse`, `transformErrorResponse`, etc.) rather than in components. This makes the logic reusable across the application.
+
+## Error Handling Patterns
+
+### RTK Query Mutations
+
+```typescript
+const [createMail, { isLoading, error }] = useCreateMailMutation()
+
+const handleSubmit = async () => {
+ try {
+ const result = await createThing(data).unwrap()
+ toast('Success!')
+ } catch (err) {
+ if ('status' in err) {
+ // FetchBaseQueryError - has status, data, error
+ const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
+ toast.error(errMsg)
+ } else {
+ // SerializedError - has message, code, name
+ toast(err.message || 'An error occurred', 'danger')
+ toast(err.message || 'An error occurred')
+ }
+ }
+}
+```
+
+### RTK Query Queries
+
+```typescript
+const { data, error, isLoading, refetch } = useThing({ id: '123' })
+
+// Display error in UI, it won't render if error is undefined
+return {error}
+
+// Retry on error
+const handleRetry = () => refetch()
+```
diff --git a/frontend/.claude/context/architecture.md b/frontend/.claude/context/architecture.md
new file mode 100644
index 000000000000..cc1aad9e47fc
--- /dev/null
+++ b/frontend/.claude/context/architecture.md
@@ -0,0 +1,28 @@
+# Architecture & Configuration
+
+## Environment Configuration
+
+- Config files: `env/project_.js`
+- Available environments: `dev`, `prod`, `staging`, `local`, `selfhosted`, `e2e`
+- Project config: `common/project.js` (imports from env files)
+- Override: `ENV=local npm run dev` or `ENV=staging npm run dev`
+
+## Key Technologies
+
+- React 16.14 + TypeScript + Bootstrap 5.2.2
+- Redux Toolkit + RTK Query (API state management)
+- Flux stores (legacy state management)
+- Webpack 5 + Express dev server
+- Sentry (error tracking)
+- Flagsmith (feature flags - this project IS Flagsmith, dogfooding its own platform)
+
+## Additional Rules
+
+- **TypeScript/ESLint**: Build may ignore some errors, but always run linting on modified files
+- **Web-specific code**: Goes in `/web/` directory (not `/common`)
+- **Redux Persist**: Whitelist in `common/store.ts`
+- **Imports**: Always use path aliases (`common/*`, `components/*`, `project/*`) - NO relative imports
+
+## Documentation
+
+Check the main repository README and docs for additional information
diff --git a/frontend/.claude/context/backend-integration.md b/frontend/.claude/context/backend-integration.md
new file mode 100644
index 000000000000..37015a63a10a
--- /dev/null
+++ b/frontend/.claude/context/backend-integration.md
@@ -0,0 +1,277 @@
+# Backend Integration Guide
+
+## Backend Repository Structure
+
+The backend API is located at `../api` (sibling directory to this frontend repo).
+
+### Key Backend Directories
+
+```
+../api/
+├── apps/
+│ ├── projects/ # Projects and features
+│ ├── environments/ # Environment management
+│ ├── features/ # Feature flags
+│ ├── segments/ # User segments
+│ ├── organisations/ # Organization management
+│ ├── users/ # User management
+│ ├── permissions/ # Access control
+│ └── audit/ # Audit logs
+└── tests/ # Test files mirror apps/ structure
+```
+
+## Finding Backend Endpoints
+
+### Method 1: Search by Feature Branch
+
+When a feature is on a specific branch:
+
+```bash
+cd ../api
+git fetch
+git log --oneline origin/feat/your-branch -n 10
+```
+
+Look for the relevant commit, then examine the changes:
+
+```bash
+# See what files were changed
+git show COMMIT_HASH --stat
+
+# View specific file at that commit
+git show COMMIT_HASH:apps/customers/urls.py
+
+# Search for specific pattern in file
+git show COMMIT_HASH:apps/customers/urls.py | grep -A 5 "invoice"
+git show COMMIT_HASH:apps/customers/views.py | grep -A 20 "def get_company_invoices"
+```
+
+### Method 2: Search Current Codebase
+
+If the feature is already merged or on main:
+
+```bash
+cd ../api
+
+# Search for URL patterns
+grep -r "path.*features" apps/*/urls.py
+
+# Search for view functions
+grep -r "def get.*feature" apps/*/views.py
+
+# Search for serializers
+grep -r "class.*FeatureSerializer" apps/*/serializers.py
+```
+
+### Method 3: Use `/backend` Command
+
+From the frontend, use the `/backend` slash command:
+
+```
+/backend feature endpoint
+```
+
+This searches the backend codebase for relevant code.
+
+## Understanding Backend Code
+
+### 1. URL Patterns (`urls.py`)
+
+Django URL patterns define the API routes:
+
+```python
+# apps/features/urls.py
+path(
+ "",
+ get_feature,
+ name="feature-detail",
+),
+```
+
+**Maps to:** `GET /api/v1/features/{feature_id}`
+
+### 2. View Functions (`views.py`)
+
+Views handle the HTTP request/response:
+
+```python
+# apps/features/views.py
+@api_view(["GET"])
+@permission_classes([IsAuthenticated])
+def get_feature(request, feature_id, *args, **kwargs):
+ feature = Feature.objects.get(id=feature_id)
+ serializer = FeatureSerializer(feature)
+ return Response(status=status.HTTP_200_OK, data=serializer.data)
+```
+
+Key things to note:
+- HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
+- Authentication required: Usually JWT or Token based
+- Permissions: `@permission_classes`
+- Path parameters: `feature_id` from URL
+- Return format: Usually `Response(data=...)`
+
+### 3. Service Layer (`services.py`)
+
+Business logic is in service classes:
+
+```python
+# apps/features/services.py (if exists)
+# Note: Flagsmith backend may not always use a service layer
+# Business logic often lives directly in views or models
+```
+
+### 4. Serializers (`serializers.py`)
+
+Serializers define the data structure:
+
+```python
+# apps/features/serializers.py
+class FeatureSerializer(serializers.ModelSerializer):
+ id = serializers.IntegerField()
+ name = serializers.CharField()
+ description = serializers.CharField()
+ type = serializers.CharField()
+ default_enabled = serializers.BooleanField()
+ created_date = serializers.DateTimeField()
+```
+
+## API Response Patterns
+
+### List Endpoints
+
+```
+GET /api/v3/resource/
+Response: [{ id: 1, ... }, { id: 2, ... }]
+```
+
+### Detail Endpoints
+
+```
+GET /api/v3/resource/{id}/
+Response: { id: 1, name: "...", ... }
+```
+
+### Nested Resources
+
+```
+GET /api/v3/admin/partners/{partner_id}/customers
+Response: [{ id: 1, name: "...", ... }, { id: 2, name: "...", ... }]
+```
+
+### Action Endpoints
+
+```
+POST /api/v3/admin/partners/{id}/activate
+Body: { commission_rate: 10.5 }
+Response: { success: true, partner: {...} }
+```
+
+## Common Partner Portal Endpoints
+
+| Resource | Endpoint | Method | Notes |
+|----------|----------|--------|-------|
+| **Partners** | `/admin/partners` | GET | List all partners |
+| | `/admin/partners/{id}` | GET | Partner detail |
+| | `/admin/partners/{id}` | PUT/PATCH | Update partner |
+| | `/admin/partners/{id}/activate` | POST | Activate partner |
+| | `/admin/partners/{id}/deactivate` | POST | Deactivate partner |
+| **Customers** | `/admin/partners/{id}/customers` | GET | List partner's customers |
+| | `/admin/customers/{id}` | GET | Customer detail |
+| **Commissions** | `/admin/partners/{id}/commissions` | GET | Partner commission history |
+| | `/admin/commissions/{id}` | GET | Commission detail |
+| **Payouts** | `/admin/partners/{id}/payouts` | GET | Partner payout history |
+| | `/admin/payouts/{id}` | GET | Payout detail |
+| | `/admin/payouts` | POST | Create new payout |
+| **Offers** | `/offers` | GET | List available offers |
+| | `/offers/{id}` | GET | Offer detail |
+| **Dashboard** | `/admin/partners/{id}/stats` | GET | Partner statistics/metrics |
+
+## Testing Backend Changes Locally
+
+### 1. Run Backend Locally
+
+```bash
+cd ../api
+# Follow backend README for setup
+python manage.py runserver
+```
+
+### 2. Point Frontend to Local Backend
+
+```bash
+# In frontend repo
+API_URL=http://localhost:8000/api/v3/ npm run dev
+```
+
+### 3. Check API Response
+
+```bash
+# Test endpoint directly
+curl -H "Authorization: Bearer YOUR_TOKEN" \
+ http://localhost:8000/api/v3/admin/partners/123
+```
+
+## Common Patterns
+
+### Permission Checks
+
+Backend often checks:
+- `@permission_classes([IsAuthenticated])` - Must be logged in
+- `@permission_classes([IsAdminUser])` - Admin user required
+- `@has_partner_permission([PartnerPermissionType.VIEW_CUSTOMERS])` - Specific permission required
+
+Frontend should mirror these checks using:
+```typescript
+const { user } = useAuth()
+const isAdmin = user?.is_admin
+const canViewCustomers = user?.permissions?.includes('VIEW_CUSTOMERS')
+```
+
+### Pagination
+
+Backend may return paginated responses:
+```json
+{
+ "count": 100,
+ "next": "https://api.../resource/?page=2",
+ "previous": null,
+ "results": [...]
+}
+```
+
+Frontend should handle with:
+```typescript
+query: (req) => ({
+ url: `resource`,
+ params: { page: req.page, page_size: 20 }
+})
+```
+
+### Error Responses
+
+Backend typically returns:
+```json
+{
+ "detail": "Error message",
+ "code": "ERROR_CODE"
+}
+```
+
+Frontend should extract `detail` for user-facing error messages.
+
+## Checklist for New Endpoints
+
+- [ ] Find endpoint in backend code (views.py, urls.py)
+- [ ] Note HTTP method (GET, POST, etc.)
+- [ ] Note path parameters (`{id}`, `{company_id}`)
+- [ ] Note query parameters (`?page=1`)
+- [ ] Check authentication requirements
+- [ ] Check permission requirements
+- [ ] Find or create matching serializer for response type
+- [ ] Check if response needs transformation (e.g., timestamps)
+- [ ] Test endpoint directly with curl or Postman
+- [ ] Add request type to `common/types/requests.ts`
+- [ ] Add/extend RTK Query service in `common/services/`
+- [ ] Use hook in component with loading/error handling
+- [ ] Run linter on modified files
diff --git a/frontend/.claude/context/feature-flags/feature-flags.md b/frontend/.claude/context/feature-flags/feature-flags.md
new file mode 100644
index 000000000000..ec7c5030d92a
--- /dev/null
+++ b/frontend/.claude/context/feature-flags/feature-flags.md
@@ -0,0 +1,190 @@
+# Feature Flags (Flagsmith)
+
+## Overview
+
+The project uses Flagsmith for feature flag management. Flags allow you to control feature visibility without deploying code changes.
+
+**IMPORTANT:** Only implement feature flags when explicitly requested by the user. By default, implement features directly without flags. Feature flags add complexity and should only be used when there's a specific need for:
+- Progressive rollouts to specific users
+- A/B testing different implementations
+- Ability to quickly disable a feature in production
+- Gradual feature adoption across user segments
+
+If the user doesn't mention feature flags, implement features directly.
+
+## Project Configuration
+
+Configuration files:
+- **Staging**: `common/project.js` (look for `flagsmith` property)
+- **Production**: `common/project_prod_*.js` (look for `flagsmith` property)
+
+### Environment-Specific Configuration
+
+See `project-config.local.md` for API keys, project IDs, and environment IDs.
+
+If the file doesn't exist, copy from `project-config.local.md.example` and fill in your values.
+
+## Setup
+
+- **Provider**: `components/FeatureFlagProvider.tsx` wraps the app
+- **Configuration**: Flagsmith environment ID in `common/project.ts`
+- **User Context**: Flags are user-specific, identified by email
+
+## Usage Pattern
+
+### Standard Pattern (Recommended)
+
+**This project uses `Utils.getFlagsmithHasFeature()` for all feature flag checks.**
+
+```typescript
+// In any component (functional or class)
+const MyComponent = () => {
+ const isFeatureEnabled = Utils.getFlagsmithHasFeature('feature_name')
+
+ return (
+ <>
+ {isFeatureEnabled && (
+
}
+ >
+ )
+ }
+}
+```
+
+### Alternative: Direct flagsmith Hook (Not Used in This Project)
+
+For reference, the project also supports direct `useFlags` hook, but **Utils.getFlagsmithHasFeature is preferred**:
+
+```typescript
+import { useFlags } from 'flagsmith/react'
+
+const MyComponent = () => {
+ const flags = useFlags(['feature_name'])
+ const isFeatureEnabled = flags.feature_name?.enabled
+
+ return (
+ <>
+ {isFeatureEnabled &&
Feature content here
}
+ >
+ )
+}
+```
+
+## Best Practices
+
+1. **Use Utils.getFlagsmithHasFeature**: This is the standard pattern used throughout the codebase
+2. **Declare flags early in render**: Define flag variables at the top of your render method or component
+3. **Conditional rendering**: Wrap new features in flag checks
+4. **Table columns**: Hide entire columns when flag is disabled (header + cells)
+5. **API calls**: Only make requests if feature flag is enabled
+6. **Naming**: Use snake_case for flag names (e.g., `download_invoices`)
+7. **For feature values**: Use `Utils.getFlagsmithJSONValue('flag_name', defaultValue)` to get JSON values
+
+## Examples
+
+### Simple Feature Toggle
+```typescript
+const isNewDashboard = Utils.getFlagsmithHasFeature('new_dashboard')
+if (isNewDashboard) {
+ return
+}
+return
+```
+
+### Progressive Feature Rollout Pattern
+
+Common pattern: Add new features behind flags while keeping existing functionality intact.
+
+```typescript
+import { useState } from 'react'
+import { Tabs } from 'components/base/forms/Tabs'
+
+const MyPage = () => {
+ const newFeatureEnabled = Utils.getFlagsmithHasFeature('new_feature')
+ const [activeTab, setActiveTab] = useState(0)
+
+ return (
+
+
Section Title
+ {newFeatureEnabled ? (
+
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+```
+
+### Table Column with Flag
+```typescript
+const canShowActions = Utils.getFlagsmithHasFeature('show_actions')
+
+return (
+
+
+
+
Name
+ {canShowActions &&
Actions
}
+
+
+
+ {data.map(item => (
+
+
{item.name}
+ {canShowActions && (
+
+ )}
+
+ ))}
+
+
+)
+```
+
+### Trait Example (User Preferences)
+```typescript
+// Traits are user-specific preferences, not feature toggles
+const flags = useFlags([], ['dark_mode'])
+const isDarkMode = flags.dark_mode // Returns boolean/string/number directly
+
+// Setting a trait
+const flagsmith = useFlagsmith()
+flagsmith.setTrait('dark_mode', true)
+```
+
+## Managing Feature Flags via MCP
+
+For MCP tools and workflows, see [mcp-workflows.md](mcp-workflows.md).
+
+## Reference Implementation
+
+See `pages/dashboard.tsx` for a complete example of:
+- Feature flag setup with `useFlags(['flag_name'])`
+- Conditional component rendering
+- Checking `.enabled` property
+- Wrapping entire components with feature flags
+
+See `components/DarkModeHandler.tsx` for an example of trait usage.
diff --git a/frontend/.claude/context/feature-flags/mcp-workflows.md b/frontend/.claude/context/feature-flags/mcp-workflows.md
new file mode 100644
index 000000000000..7eebc605eeaf
--- /dev/null
+++ b/frontend/.claude/context/feature-flags/mcp-workflows.md
@@ -0,0 +1,179 @@
+# Flagsmith MCP Workflows
+
+This project uses the **flagsmith-admin-api MCP** for feature flag management. All operations are performed through MCP tools instead of manual API calls or web console.
+
+## ⚠️ CRITICAL: Project Verification Before Creating Flags
+
+**STOP! Before creating ANY feature flag, follow this mandatory checklist:**
+
+1. ✅ **Verify the correct project**: Check `project-config.local.md` for the correct project ID
+2. ✅ **Read configuration**: Check `common/project.js` to see the Flagsmith API key
+3. ✅ **Confirm with MCP**: Run `mcp__flagsmith__list_project_environments` with the project_id from config to verify
+4. ✅ **Create ONCE**: Only call `mcp__flagsmith__create_feature` ONCE with the correct project_id
+
+**NEVER:**
+- ❌ Create flags in multiple projects to "try" which one is correct
+- ❌ Guess the project ID without verification
+- ❌ Create duplicate flags
+
+**Why this matters:** Creating flags in the wrong project pollutes other Flagsmith projects with incorrect flags that don't belong there. This is a critical error.
+
+## Known Limitations
+
+**IMPORTANT:** Published feature versions cannot be modified via the Flagsmith API.
+
+- After creating a flag with MCP (`mcp__flagsmith__create_feature`), the flag is created but disabled by default
+- To enable/disable the flag in specific environments, you must use the Flagsmith web UI at https://app.flagsmith.com
+- This is a Flagsmith API limitation, not a tooling issue
+- The MCP can create flags, but enabling/disabling must be done manually via the UI
+
+**Workflow:**
+1. Create flag via MCP → ✅ Automated
+2. Implement code with `useFlags()` → ✅ Automated
+3. Enable flag in staging/production → ❌ Manual (via Flagsmith UI)
+
+When documenting completion, always inform the user that step 3 requires manual action via the web UI.
+
+## CRITICAL: When User Says "Create a Feature Flag"
+
+**When the user requests to create a feature flag, you MUST:**
+
+1. ✅ **Actually create the flag in Flagsmith** using `mcp__flagsmith__create_feature`
+2. ✅ **Implement the frontend code** that uses the flag with `useFlags()`
+3. ✅ **Return the flag details** (ID, name, project) to confirm creation
+
+**DO NOT:**
+- ❌ Only implement the code without creating the flag
+- ❌ Assume the flag already exists
+- ❌ Assume the user will create it manually
+
+**This is a two-part task:**
+- **Backend (Flagsmith)**: Create the flag entity in Flagsmith
+- **Frontend (Code)**: Write code that checks the flag with `useFlags()`
+
+Both parts are required when "create a feature flag" is requested.
+
+## Standard Workflow Example
+
+```
+User: "Add a download button, create this under a feature flag download_invoices"
+
+Step 1: Create flag in Flagsmith
+ - Use mcp__flagsmith__list_organizations (if needed)
+ - Use mcp__flagsmith__list_projects_in_organization (find project)
+ - Use mcp__flagsmith__create_feature with name "download_invoices"
+ - Confirm flag ID and status to user
+
+Step 2: Implement code
+ - Add useFlags(['download_invoices']) to component
+ - Wrap button with flag check: {flags.download_invoices?.enabled && }
+ - Test that code compiles
+
+Step 3: Report completion
+ - Confirm flag created in Flagsmith (with ID)
+ - Confirm code implementation complete
+```
+
+## Available MCP Tools
+
+The MCP provides tools prefixed with `mcp__flagsmith-admin-api__` for managing feature flags.
+
+### Discovery & Listing
+- **`list_organizations`** - List all organizations accessible with your API key
+- **`list_projects_in_organization`** - List all projects in an organization
+- **`list_project_features`** - List all feature flags in a project
+- **`list_project_environments`** - List all environments (staging, production, etc.)
+- **`list_project_segments`** - List user segments for targeting
+
+### Feature Flag Operations
+- **`create_feature`** - Create a new feature flag (defaults to disabled)
+- **`get_feature`** - Get detailed information about a specific flag
+- **`update_feature`** - Update flag name or description
+- **`get_feature_evaluation_data`** - Get analytics/metrics for a flag
+- **`get_feature_external_resources`** - Get linked resources (Jira, GitHub, etc.)
+- **`get_feature_code_references`** - Get code usage information
+
+### Feature State Management
+- **`get_environment_feature_versions`** - Get version info for a flag in an environment
+- **`get_environment_feature_version_states`** - Get state info for a specific version
+- **`create_environment_feature_version_state`** - Create new state (enable/disable/set value)
+- **`update_environment_feature_version_state`** - Update existing state
+- **`patch_environment_feature_version_state`** - Partially update state
+
+### Advanced Features
+- **`create_multivariate_option`** - Create A/B test variants
+- **`list_multivariate_options`** - List all variants for a flag
+- **`update_multivariate_option`** / **`delete_multivariate_option`** - Manage variants
+- **`create_project_segment`** - Create user targeting rules
+- **`update_project_segment`** / **`get_project_segment`** - Manage segments
+- **`list_project_change_requests`** - List change requests for approval workflows
+- **`create_environment_change_request`** - Create controlled deployment requests
+- **`list_project_release_pipelines`** - List automated deployment pipelines
+
+## Common Workflows
+
+### 1. Find Your Project
+```
+Step 1: List organizations
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_organizations
+
+Step 2: List projects in your organization
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_projects_in_organization
+Parameters: {"org_id": }
+
+Step 3: Find project by matching repository name to project name
+```
+
+### 2. List Existing Feature Flags
+```
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_project_features
+Parameters: {"project_id": }
+Optional: Add query params for pagination: {"page": 1, "page_size": 50}
+```
+
+### 3. Create a New Feature Flag
+```
+Step 1: Create the flag (disabled by default)
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_create_feature
+Parameters:
+ pathParameters: {"project_id": }
+ body: {"name": "flag_name", "description": "Description"}
+
+Step 2: Get environment IDs
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_project_environments
+Parameters: {"project_id": }
+
+Step 3: Enable for staging/development
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_get_environment_feature_versions
+Then use create/update_environment_feature_version_state to enable
+```
+
+### 4. Enable/Disable a Flag in an Environment
+```
+Step 1: Get feature versions
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_get_environment_feature_versions
+Parameters: {"environment_id": , "feature_id": }
+
+Step 2: Update feature state
+Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_patch_environment_feature_version_state
+Parameters:
+ pathParameters: {"environment_id": , "feature_id": , "version_id": }
+ body: {"enabled": true}
+```
+
+## Best Practices
+
+1. **Always look up IDs dynamically** - Don't hardcode organization, project, or feature IDs
+2. **Match repository to project** - Project names typically correspond to repository names
+3. **Start disabled** - New flags are created disabled by default
+4. **Enable in staging first** - Test in non-production environments before enabling in production
+5. **Use descriptive names** - Follow snake_case naming: `download_invoices`, `new_dashboard`
+6. **Document usage** - Note which components use each flag
+
+## Environment-Specific Configuration
+
+When creating a new feature flag:
+1. **Create the flag** (disabled globally by default)
+2. **Enable for staging/development** to allow testing
+3. **Keep production disabled** until ready for release
+4. **Use change requests** for production changes if approval workflows are configured
diff --git a/frontend/.claude/context/feature-flags/project-config.local.md.example b/frontend/.claude/context/feature-flags/project-config.local.md.example
new file mode 100644
index 000000000000..cc1436e67556
--- /dev/null
+++ b/frontend/.claude/context/feature-flags/project-config.local.md.example
@@ -0,0 +1,25 @@
+# Flagsmith Project Configuration
+
+Copy this file to `project-config.local.md` and fill in your values.
+
+**Flagsmith Organization:** (ID: )
+
+**Project: (ID: )**
+- **Production Environment**
+ - ID:
+ - API Key: ``
+ - Requires approval for changes (minimum_change_request_approvals: 1)
+- **Staging Environment**
+ - ID:
+ - API Key: ``
+ - No approval required for changes
+
+**CRITICAL: ALWAYS USE PROJECT ID WHEN CREATING FEATURE FLAGS FOR THIS FRONTEND.**
+
+**BEFORE CREATING ANY FEATURE FLAG, YOU MUST:**
+1. Read `common/project.js` and verify the `flagsmith` API key
+2. Use `mcp__flagsmith__list_project_environments` with project_id: to confirm the API key matches
+3. **ONLY** create the flag in Project ID ()
+4. **NEVER** create flags in multiple projects or guess which project to use
+
+**Creating flags in the wrong project is a critical error that pollutes other projects with incorrect flags.**
diff --git a/frontend/.claude/context/forms.md.draft b/frontend/.claude/context/forms.md.draft
new file mode 100644
index 000000000000..416d229cb983
--- /dev/null
+++ b/frontend/.claude/context/forms.md.draft
@@ -0,0 +1,130 @@
+# Form Patterns (Yup + Formik)
+
+## Standard Pattern for ALL Forms
+
+```typescript
+import { useFormik } from 'formik'
+import * as yup from 'yup'
+import { validateForm } from 'project/utils/forms/validateForm'
+
+const schema = yup.object().shape({
+ name: yup.string().required('Name is required'),
+})
+
+const MyForm = () => {
+ const { errors, touched, values, handleChange, handleBlur, handleSubmit, setTouched } = useFormik({
+ initialValues: { name: '' },
+ onSubmit: async (values) => { /* API call */ },
+ validationSchema: schema,
+ validateOnMount: true,
+ })
+
+ const onSubmit = async (e) => {
+ e.preventDefault()
+ const isValid = await validateForm(errors, setTouched)
+ if (isValid) handleSubmit()
+ }
+
+ return (
+
+ )
+}
+```
+
+## Form Components (in `components/base/forms/`)
+
+- **InputGroup**: Standard wrapper - pass `touched` and `error` props
+- **DatePicker**, **PhoneInput**, **Select**: Use with `component` prop on InputGroup
+- **Radio**, **Checkbox**, **Switch**: Boolean/choice inputs
+
+**Reference**: See `/examples/forms/ComprehensiveFormExample.tsx`
+
+## Form Spacing & Layout
+
+### Standard Form Layout Structure
+
+```tsx
+
+
+
Form Title
+
+
+
+
+
+
+
+
+```
+
+### Spacing Classes
+
+Use Bootstrap's spacing scale consistently:
+
+- **Between InputGroups**: `d-flex flex-column gap-4` (24px vertical gap)
+- **Section header margin**: `h3 className='mb-4 pb-3 border-bottom'` (24px bottom)
+- **Two-column rows**: `d-flex gap-4 mb-4` with `flex-1` per column
+- **Button rows**: `d-flex justify-content-end gap-2 mt-3` (8px between buttons, 16px top margin)
+- **Error messages**: `mb-5` when standalone (48px)
+
+### Multi-Section Forms
+
+For forms with multiple sections:
+
+```tsx
+
+```
+
+### Bootstrap Gap/Margin Scale
+
+- `gap-2` = 0.5rem (8px)
+- `gap-3` = 1rem (16px)
+- `gap-4` = 1.5rem (24px) ← Use for InputGroup spacing
+- `gap-5` = 3rem (48px) ← Use for section separation
+
+- `mb-3` = 1rem (16px)
+- `mb-4` = 1.5rem (24px) ← Use for section headers
+- `mb-5` = 3rem (48px) ← Use for standalone errors/content
+
+### Key Files with Examples
+
+- `/components/examples/forms/ComprehensiveFormExample.tsx` - Full pattern with sections
+- `/components/ChangeAccountInformation.tsx` - Account form spacing
+- `/components/ChangeContact.tsx` - Contact form spacing
+- `/components/whatsapp/CreateEditNumber.tsx` - Modal form pattern
diff --git a/frontend/.claude/context/git-workflow.md b/frontend/.claude/context/git-workflow.md
new file mode 100644
index 000000000000..7d26427c6e23
--- /dev/null
+++ b/frontend/.claude/context/git-workflow.md
@@ -0,0 +1,29 @@
+# Git Workflow
+
+## Pre-Commit Checking Strategy
+
+Before creating commits, always check and lint staged files to catch errors early:
+
+```bash
+npm run check:staged
+```
+
+Or use the slash command:
+```
+/check-staged
+```
+
+This runs both typechecking and linting on staged files only, mimicking pre-commit hooks.
+
+## Available Scripts
+
+- `npm run check:staged` - Typecheck + lint staged files (use this!)
+- `npm run typecheck:staged` - Typecheck staged files only
+- `npm run lint:staged` - Lint staged files only (with --fix)
+
+## Important Notes
+
+- Never run `npm run typecheck` (full project) or `npm run lint` on all files unless explicitly requested
+- Always focus on staged files only to keep checks fast and relevant
+- The lint:staged script auto-fixes issues where possible
+- Fix any remaining type errors or lint issues before committing
diff --git a/frontend/.claude/context/patterns/api.md b/frontend/.claude/context/patterns/api.md
new file mode 100644
index 000000000000..b0f7a3e6da93
--- /dev/null
+++ b/frontend/.claude/context/patterns/api.md
@@ -0,0 +1,207 @@
+# API Service Patterns
+
+## Query vs Mutation Rule
+
+- **GET requests** → `builder.query`
+- **POST/PUT/PATCH/DELETE requests** → `builder.mutation`
+
+```typescript
+// ✅ Correct: GET endpoint
+getMailItem: builder.query({
+ providesTags: (res, _, req) => [{ id: req?.id, type: 'MailItem' }],
+ query: (query: Req['getMailItem']) => ({
+ url: `mailbox/mails/${query.id}`,
+ }),
+}),
+
+// ✅ Correct: POST endpoint
+createScanMail: builder.mutation({
+ invalidatesTags: [{ id: 'LIST', type: 'ScanMail' }],
+ query: (query: Req['createScanMail']) => ({
+ body: query,
+ method: 'POST',
+ url: `mailbox/mails/${query.id}/actions/scan`,
+ }),
+}),
+```
+
+## File Download Pattern
+
+Use the reusable `handleFileDownload` utility for endpoints that return files:
+
+```typescript
+import { handleFileDownload } from 'common/utils/fileDownload'
+
+getInvoiceDownload: builder.query({
+ query: (query: Req['getInvoiceDownload']) => ({
+ url: `customers/invoices/${query.id}/download`,
+ responseHandler: (response) => handleFileDownload(response, 'invoice.pdf'),
+ }),
+}),
+```
+
+## Pagination Pattern
+
+Use `useInfiniteScroll` hook for paginated lists:
+
+```typescript
+import useInfiniteScroll from 'common/hooks/useInfiniteScroll'
+import { useGetMailQuery } from 'common/services/useMail'
+
+const MailList = ({ subscription_id }: Props) => {
+ const {
+ data,
+ isLoading,
+ isFetching,
+ loadMore,
+ refresh,
+ searchItems,
+ } = useInfiniteScroll(
+ useGetMailQuery,
+ { subscription_id, page_size: 20 },
+ )
+
+ return (
+ }
+ refreshFunction={refresh}
+ pullDownToRefresh
+ >
+ {data?.results.map(item => )}
+
+ )
+}
+```
+
+## Error Handling
+
+### RTK Query Error Pattern
+
+```typescript
+const [createMail, { isLoading, error }] = useCreateMailMutation()
+
+const handleSubmit = async () => {
+ try {
+ const result = await createMail(data).unwrap()
+ // Success - result contains the response
+ toast.success('Mail created successfully')
+ } catch (err) {
+ // Error handling
+ if ('status' in err) {
+ // FetchBaseQueryError
+ const errMsg = 'error' in err ? err.error : JSON.stringify(err.data)
+ toast.error(errMsg)
+ } else {
+ // SerializedError
+ toast.error(err.message || 'An error occurred')
+ }
+ }
+}
+```
+
+### Query Refetching
+
+```typescript
+const { data, refetch } = useGetMailQuery({ id: '123' })
+
+// Refetch on demand
+const handleRefresh = () => {
+ refetch()
+}
+
+// Automatic refetch on focus/reconnect is enabled by default in common/service.ts
+```
+
+## Cache Invalidation
+
+### Manual Cache Clearing
+
+```typescript
+import { getStore } from 'common/store'
+import { mailItemService } from 'common/services/useMailItem'
+
+export const clearMailCache = () => {
+ getStore().dispatch(
+ mailItemService.util.invalidateTags([{ type: 'MailItem', id: 'LIST' }])
+ )
+}
+```
+
+### Automatic Invalidation
+
+Cache invalidation is handled automatically through RTK Query tags:
+
+```typescript
+// Mutation invalidates the list
+createMail: builder.mutation({
+ invalidatesTags: [{ type: 'Mail', id: 'LIST' }],
+ // This will automatically refetch any active queries with matching tags
+}),
+```
+
+## Type Organization
+
+### Request and Response Types
+
+All API types go in `common/types/`:
+
+```typescript
+// common/types/requests.ts
+export type Req = {
+ getMail: PagedRequest<{
+ subscription_id: string
+ q?: string
+ }>
+ createMail: {
+ id: string
+ content: string
+ }
+ // END OF TYPES
+}
+
+// common/types/responses.ts
+export type Res = {
+ mail: PagedResponse
+ mailItem: MailItem
+ // END OF TYPES
+}
+```
+
+### Shared Types
+
+For types used across requests AND responses, keep them in their respective files but document the shared usage:
+
+```typescript
+// common/types/requests.ts
+export type Address = {
+ address_line_1: string
+ address_line_2: string | null
+ postal_code: string
+ city: string
+ country: string
+}
+```
+
+## SSG CLI Usage
+
+Always use `npx ssg` to generate new API services:
+
+```bash
+# Interactive mode
+npx ssg
+
+# Follow prompts to:
+# 1. Choose action type (get/create/update/delete)
+# 2. Enter resource name
+# 3. Enter API endpoint URL
+# 4. Configure cache invalidation
+```
+
+The CLI will:
+- Create/update service file in `common/services/`
+- Add types to `common/types/requests.ts` and `responses.ts`
+- Generate appropriate hooks (Query or Mutation)
+- Use correct import paths (no relative imports)
diff --git a/frontend/.claude/context/patterns/index.md b/frontend/.claude/context/patterns/index.md
new file mode 100644
index 000000000000..f47d84d40664
--- /dev/null
+++ b/frontend/.claude/context/patterns/index.md
@@ -0,0 +1,167 @@
+# Common Code Patterns
+
+> For API patterns, see [api.md](api.md). For mobile patterns, see [mobile.md](mobile.md).
+
+## Complete Feature Implementation Example
+
+This end-to-end example shows how to add tabs with a new API endpoint (real implementation from the codebase).
+
+**Requirements:** Add a "Top-Up" invoices tab to the account-billing page, pulling from a new backend endpoint.
+
+### Step 1: Check Backend API
+
+```bash
+cd ../api
+git fetch
+git show COMMIT_HASH:apps/customers/urls.py | grep "invoice"
+# Found: path("companies//invoices", get_company_invoices)
+```
+
+### Step 2: Add Request Type
+
+**File:** `common/types/requests.ts`
+
+```typescript
+export type Req = {
+ // ... existing types
+ getCompanyInvoices: {
+ company_id: string
+ }
+}
+```
+
+### Step 3: Extend RTK Query Service
+
+**File:** `common/services/useInvoice.ts`
+
+```typescript
+export const invoiceService = service
+ .enhanceEndpoints({ addTagTypes: ['Invoice'] })
+ .injectEndpoints({
+ endpoints: (builder) => ({
+ getCompanyInvoices: builder.query<
+ Res['invoices'],
+ Req['getCompanyInvoices']
+ >({
+ providesTags: [{ id: 'LIST', type: 'Invoice' }],
+ query: (req) => ({
+ url: `customers/companies/${req.company_id}/invoices`,
+ }),
+ transformResponse(res: InvoiceSummary[]) {
+ return res?.map((v) => ({ ...v, date: v.date * 1000 }))
+ },
+ }),
+ }),
+ })
+
+export const {
+ useGetCompanyInvoicesQuery,
+ // END OF EXPORTS
+} = invoiceService
+```
+
+### Step 4: Create Table Component
+
+**File:** `components/project/tables/CompanyInvoiceTable.tsx`
+
+```typescript
+import { useGetCompanyInvoicesQuery } from 'common/services/useInvoice'
+import { useDefaultSubscription } from 'common/services/useDefaultSubscription'
+
+const CompanyInvoiceTable: FC = () => {
+ const { subscriptionDetail } = useDefaultSubscription()
+ const companyId = subscriptionDetail?.company_id
+
+ const { data: invoices, error, isLoading } = useGetCompanyInvoicesQuery(
+ { company_id: `${companyId}` },
+ { skip: !companyId }
+ )
+
+ if (isLoading) return
+ if (error) return {error}
+
+ return (
+
+
+
+ )
+}
+```
+
+### Responsive Tables
+
+Use Bootstrap classes for responsive behavior:
+- `d-none d-md-table-cell` - Hide column on mobile
+- `d-block d-md-none` - Show on mobile only
+
+## Tabs Component
+
+**Location:** `components/base/forms/Tabs.tsx`
+
+### Basic Usage
+
+```typescript
+import { useState } from 'react'
+import { Tabs } from 'components/base/forms/Tabs'
+
+const MyPage = () => {
+ const [activeTab, setActiveTab] = useState(0)
+
+ return (
+
+
Tab 1 content
+
Tab 2 content
+
Tab 3 content
+
+ )
+}
+```
+
+### Tabs with Feature Flag (Optional)
+
+**Note:** Only use feature flags when explicitly requested. By default, implement features directly without flags.
+
+When specifically requested, this pattern shows tabs only when feature flag is enabled:
+
+```typescript
+import { useFlags } from 'flagsmith/react'
+import { Tabs } from 'components/base/forms/Tabs'
+import Utils from 'common/utils/utils'
+const MyPage = () => {
+ const my_feature_flag = Utils.getFlagsmithHasFeature('my_feature_flag')
+ const [activeTab, setActiveTab] = useState(0)
+
+ return (
+
+
My Section
+ {my_feature_flag? (
+
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+```
+
+See `feature-flags/` for more details on when and how to use feature flags.
+
+### Uncontrolled Tabs
+
+For simple cases without parent state management:
+
+```typescript
+
+
Tab 1 content
+
Tab 2 content
+
+```
+
+## Confirmation Dialogs
+
+**NEVER use `window.confirm`** - Always use the `openConfirm` function from `components/base/Modal`.
+
+### Correct Usage
+
+```typescript
+import { openConfirm } from 'components/base/Modal'
+
+// Signature: openConfirm(title, body, onYes, onNo?, challenge?)
+openConfirm({
+ body: 'Closing this will discard your unsaved changes.',
+ noText: 'Cancel',
+ onNo: () => resolve(false),
+ onYes: () => resolve(true),
+ title: 'Discard changes',
+ yesText: 'Ok',
+})
+```
+
+### Parameters
+- `title: string` - Dialog title
+- `body: ReactNode` - Dialog content (can be JSX)
+- `onYes: (closeModal: () => void) => void` - Callback when user confirms
+- `onNo?: () => void` - Optional callback when user cancels
+- `challenge?: string` - Optional challenge text user must type to confirm
+
+### Key Points
+- The `onYes` callback receives a `closeModal` function
+- Always call `closeModal()` when the action completes successfully
+- Can be async - use `async (closeModal) => { ... }`
diff --git a/frontend/.claude/scripts/sync-types-helper.py b/frontend/.claude/scripts/sync-types-helper.py
new file mode 100755
index 000000000000..c9e6794fb35d
--- /dev/null
+++ b/frontend/.claude/scripts/sync-types-helper.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+"""
+Helper script for API type syncing operations.
+Minimizes token usage by batching cache operations.
+"""
+
+import json
+import sys
+from pathlib import Path
+from typing import Dict, List
+
+CACHE_FILE = Path(__file__).parent.parent / "api-type-map.json"
+
+
+def load_cache() -> Dict:
+ """Load the type cache from JSON file."""
+ if not CACHE_FILE.exists():
+ return {"_metadata": {}, "response_types": {}, "request_types": {}}
+ with open(CACHE_FILE, "r") as f:
+ cache = json.load(f)
+ # Migrate old format to new format if needed
+ if "types" in cache and "response_types" not in cache:
+ cache["response_types"] = cache.pop("types")
+ cache["request_types"] = {}
+ return cache
+
+
+def save_cache(cache: Dict) -> None:
+ """Save the type cache to JSON file."""
+ with open(CACHE_FILE, "w") as f:
+ json.dump(cache, f, indent=2)
+ f.write("\n")
+
+
+def get_changed_serializers(
+ old_commit: str, new_commit: str, api_path: str
+) -> List[str]:
+ """Get list of serializer files changed between commits."""
+ import subprocess
+
+ result = subprocess.run(
+ ["git", "diff", f"{old_commit}..{new_commit}", "--name-only"],
+ cwd=api_path,
+ capture_output=True,
+ text=True,
+ )
+
+ if result.returncode != 0:
+ return []
+
+ files = result.stdout.strip().split("\n")
+ return [f for f in files if "serializers.py" in f]
+
+
+def find_types_using_serializer(
+ cache: Dict, serializer_path: str, serializer_name: str
+) -> List[str]:
+ """Find all type keys that use a specific serializer."""
+ search_string = f"{serializer_path}:{serializer_name}"
+ types = []
+
+ for key, value in cache.items():
+ if key == "_metadata":
+ continue
+ if value.get("serializer", "").startswith(search_string.split(":")[0]):
+ if serializer_name in value.get("serializer", ""):
+ types.append(key)
+
+ return types
+
+
+def update_metadata(stats: Dict) -> None:
+ """Update cache metadata with sync statistics."""
+ cache = load_cache()
+
+ if "_metadata" not in cache:
+ cache["_metadata"] = {}
+
+ cache["_metadata"].update(stats)
+ save_cache(cache)
+
+
+def get_types_needing_sync(
+ serializer_files: List[str], api_path: str, type_category: str = "response"
+) -> List[Dict]:
+ """
+ Get list of types that need syncing based on changed serializer files.
+
+ Args:
+ serializer_files: List of changed serializer file paths
+ api_path: Path to backend API repository
+ type_category: Either "response" or "request"
+
+ Returns:
+ List of dicts with type info: {key, serializer_file, serializer_class, type_name}
+ """
+ cache = load_cache()
+ types_to_check = []
+
+ # Select the appropriate cache section
+ cache_key = f"{type_category}_types"
+ type_cache = cache.get(cache_key, {})
+
+ for file_path in serializer_files:
+ # Extract serializer classes from the file path in cache
+ for type_key, type_data in type_cache.items():
+ if type_key == "_metadata":
+ continue
+
+ serializer = type_data.get("serializer", "")
+ if file_path in serializer and ":" in serializer:
+ serializer_class = serializer.split(":")[-1].strip()
+ types_to_check.append(
+ {
+ "key": type_key,
+ "serializer_file": file_path,
+ "serializer_class": serializer_class,
+ "type_name": type_data.get("type", ""),
+ }
+ )
+
+ return types_to_check
+
+
+def filter_syncable_types(cache: Dict, type_category: str = "response") -> List[Dict]:
+ """
+ Filter cache to only include types with Django serializers (exclude custom/ChargeBee/empty).
+
+ Args:
+ cache: Full cache dict
+ type_category: Either "response" or "request"
+
+ Returns:
+ List of type info dicts
+ """
+ syncable = []
+ cache_key = f"{type_category}_types"
+ type_cache = cache.get(cache_key, {})
+
+ for type_key, type_data in type_cache.items():
+ if type_key == "_metadata":
+ continue
+
+ serializer = type_data.get("serializer", "")
+ note = type_data.get("note", "")
+
+ # Skip custom responses, ChargeBee, NOT_IMPLEMENTED, and view methods
+ if any(x in note.lower() for x in ["custom", "chargebee", "empty"]):
+ continue
+ if "NOT_IMPLEMENTED" in serializer:
+ continue
+ if "views.py:" in serializer and "(" in serializer:
+ continue
+
+ # Only include Django serializers
+ if "serializers.py:" in serializer and ":" in serializer:
+ parts = serializer.split(":")
+ if len(parts) == 2:
+ syncable.append(
+ {
+ "key": type_key,
+ "serializer_file": parts[0],
+ "serializer_class": parts[1].strip(),
+ "type_name": type_data.get("type", ""),
+ }
+ )
+
+ return syncable
+
+
+def get_last_commit() -> str:
+ """Get the last backend commit hash from cache metadata."""
+ cache = load_cache()
+ return cache.get("_metadata", {}).get("lastBackendCommit", "")
+
+
+if __name__ == "__main__":
+ # Command-line interface
+ command = sys.argv[1] if len(sys.argv) > 1 else "help"
+
+ if command == "changed-serializers":
+ # Usage: python sync-types-helper.py changed-serializers OLD_COMMIT NEW_COMMIT API_PATH
+ old_commit = sys.argv[2]
+ new_commit = sys.argv[3]
+ api_path = sys.argv[4]
+ changed = get_changed_serializers(old_commit, new_commit, api_path)
+ print("\n".join(changed))
+
+ elif command == "types-to-sync":
+ # Usage: python sync-types-helper.py types-to-sync [response|request] FILE1 FILE2 ... API_PATH
+ type_category = sys.argv[2] if len(sys.argv) > 2 else "response"
+ files = sys.argv[3:]
+ api_path = sys.argv[-1] if files else ""
+ types = get_types_needing_sync(files[:-1], api_path, type_category)
+ print(json.dumps(types, indent=2))
+
+ elif command == "update-metadata":
+ # Usage: echo '{"lastSync": "..."}' | python sync-types-helper.py update-metadata
+ stats = json.load(sys.stdin)
+ update_metadata(stats)
+ print("Metadata updated")
+
+ elif command == "syncable-types":
+ # Usage: python sync-types-helper.py syncable-types [response|request]
+ type_category = sys.argv[2] if len(sys.argv) > 2 else "response"
+ cache = load_cache()
+ types = filter_syncable_types(cache, type_category)
+ print(json.dumps(types, indent=2))
+
+ elif command == "get-last-commit":
+ # Usage: python sync-types-helper.py get-last-commit
+ commit = get_last_commit()
+ print(commit)
+
+ else:
+ print("Usage:")
+ print(
+ " changed-serializers OLD NEW PATH - Get changed serializer files"
+ )
+ print(
+ " types-to-sync [response|request] FILE... PATH - Get types needing sync"
+ )
+ print(
+ " update-metadata - Update metadata (JSON via stdin)"
+ )
+ print(
+ " syncable-types [response|request] - Get all syncable type info"
+ )
+ print(
+ " get-last-commit - Get last backend commit from cache"
+ )
diff --git a/frontend/.claude/settings.json b/frontend/.claude/settings.json
new file mode 100644
index 000000000000..9367039aa0cc
--- /dev/null
+++ b/frontend/.claude/settings.json
@@ -0,0 +1,71 @@
+{
+ "autoApprovalSettings": {
+ "enabled": true,
+ "rules": [
+ {
+ "tool": "Read",
+ "pattern": "**/*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run typecheck:*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run typecheck:staged"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "node:*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run lint:fix:*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run build:*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run lint*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run check:staged"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npm run test*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "git diff*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "git log*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "git status*"
+ },
+ {
+ "tool": "Bash",
+ "pattern": "npx ssg*"
+ },
+ {
+ "tool": "WebSearch",
+ "pattern": "*"
+ },
+ {
+ "tool": "Glob",
+ "pattern": "*"
+ },
+ {
+ "tool": "Grep",
+ "pattern": "*"
+ }
+ ]
+ }
+}
diff --git a/frontend/.env-example b/frontend/.env-example
new file mode 100644
index 000000000000..3c0d775e8bd0
--- /dev/null
+++ b/frontend/.env-example
@@ -0,0 +1,5 @@
+E2E_TEST_TOKEN_DEV=
+E2E_TEST_TOKEN_LOCAL=
+E2E_TEST_TOKEN_PROD=
+E2E_TEST_TOKEN_STAGING=
+MCP_RELEASE_MANAGER_ADMIN_API_API_KEY_AUTH=
diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md
new file mode 100644
index 000000000000..a43d3624f4e8
--- /dev/null
+++ b/frontend/AGENTS.md
@@ -0,0 +1 @@
+Read @CLAUDE.md
diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
new file mode 100644
index 000000000000..49d43e6ea0d7
--- /dev/null
+++ b/frontend/CLAUDE.md
@@ -0,0 +1,39 @@
+# CLAUDE.md
+
+## Commands
+- `npm run dev` - Start dev server
+- `npm run typecheck` - Type checking
+
+## Structure
+- `/common` - Shared Redux/API (no web/mobile code)
+- `/web/components` - React components (includes `/web/components/pages/` for all page components)
+- `/web/components/pages/` - **All page components** (e.g., `FeaturesPage.js`, `ProjectSettingsPage.js`)
+- `/common/types/` - `requests.ts` and `responses.ts` for API types
+- Ignore: `ios/`, `android/`, `.net/`, `*.md.draft`
+
+## Rules
+1. **API Integration**: Use `npx ssg` CLI + check `../api` backend
+2. **Imports**: Use `common/`, `components/`, `project/` (NO relative imports)
+3. **State**: Redux Toolkit + RTK Query, store in `common/store.ts`
+4. **Feature Flags**: When user says "create a feature flag", you MUST: (1) Create it in Flagsmith using MCP tools (`mcp__flagsmith__create_feature`), (2) Implement code with `useFlags` hook. See `.claude/context/feature-flags/` for details
+5. **Linting**: ALWAYS run `npx eslint --fix ` on any files you modify
+6. **Type Enums**: Extract inline union types to named types (e.g., `type Status = 'A' | 'B'` instead of inline)
+7. **NO FETCH**: NEVER use `fetch()` directly - ALWAYS use RTK Query mutations/queries (inject endpoints into services in `common/services/`), see api-integration context
+
+## Key Files
+- Store: `common/store.ts`
+- Base service: `common/service.ts`
+
+## Context Files
+
+The `.claude/context/` directory contains **required patterns and standards** for this codebase. These are not optional suggestions - they document how things must be done in this project.
+
+For detailed guidance on specific topics:
+- **Quick Start**: `.claude/context/quick-reference.md` - Common tasks, commands, patterns
+- **API Integration**: `.claude/context/api-integration.md` - Adding endpoints, RTK Query (required reading for API work)
+- **Backend**: `.claude/context/backend-integration.md` - Finding endpoints, backend structure
+- **UI Patterns**: `.claude/context/ui-patterns.md` - Tables, tabs, modals, confirmations (required reading for UI work)
+- **Feature Flags**: `.claude/context/feature-flags/` - Using Flagsmith flags (optional, only when requested)
+- **Code Patterns**: `.claude/context/patterns/` - Complete examples, best practices
+
+**Tip:** Start with `quick-reference.md` for common tasks and checklists.
diff --git a/frontend/README.md b/frontend/README.md
index 8d23cbd8c842..69e9c308d3a6 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,3 +1,73 @@
## Flagsmith Frontend
-TODO
+### Docker-based development
+
+To bring up the API and database via Docker Compose:
+
+```bash
+curl -o docker-compose.yml https://raw.githubusercontent.com/Flagsmith/flagsmith/main/docker-compose.yml
+docker-compose -f docker-compose.yml up
+```
+
+The application will bootstrap an admin user, organisation, and project for you. You'll find a link to set your password in your Compose logs:
+
+```txt
+Superuser "admin@example.com" created successfully.
+Please go to the following page and choose a password: http://localhost:8000/password-reset/confirm/.../...
+```
+
+### Local development
+
+The project assumes the following tools installed:
+- [Node.js](https://nodejs.org/) version 22.x
+- [npm](https://www.npmjs.com/) version 10.x
+
+To install dependencies, run `npm install`.
+
+The API must be running on localhost:8000 (either via Docker or `make serve` in `../api`).
+
+To bring up a dev server, run `ENV=local npm run dev`.
+
+To run linters, run `npm run lint` (or `npm run lint:fix` to auto-fix).
+
+To run type checking, run `npm run typecheck`.
+
+### Environment configuration
+
+Environment configuration is defined in `project_*.js` files (`common/project.js` for defaults, `env/project_*.js` for staging/prod/selfhosted), selected at build time based on the target environment. All configs support runtime overrides via `globalThis.projectOverrides`, allowing deployment-time customisation without rebuilding.
+
+The `bin/env.js` script copies the appropriate `env/project_${ENV}.js` to `common/project.js`:
+- `npm run dev` → copies `project_dev.js` (staging API)
+- `ENV=local npm run dev` → copies `project_local.js` (localhost)
+- `ENV=prod npm run bundle` → copies `project_prod.js` (production)
+
+For a full list of frontend environment variables, see the [Flagsmith documentation](https://docs.flagsmith.com/deployment/hosting/locally-frontend#environment-variables).
+
+### Code guidelines
+
+#### Testing
+
+This codebase uses TestCafe for end-to-end testing. Tests are located in the `e2e/` directory.
+
+To run E2E tests (requires the API running on localhost:8000), run `npm run test`.
+
+#### Typing
+
+This codebase uses TypeScript. Run `npm run typecheck` to check for type errors.
+
+We encourage adding types to new code and improving types in existing code when working nearby.
+
+#### Design and architecture
+
+The frontend is organised into:
+- `common/` - Shared code (Redux store, RTK Query services, types, utilities)
+- `web/components/` - React components
+- `web/components/pages/` - Page-level components
+
+State management uses Redux Toolkit with RTK Query for API calls. Services are defined in `common/services/`.
+
+API types are centralised in:
+- `common/types/requests.ts` - Request types
+- `common/types/responses.ts` - Response types
+
+For AI-assisted development, see [CLAUDE.md](https://github.com/Flagsmith/flagsmith/blob/main/frontend/CLAUDE.md).
diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts
index c0371f35abd7..2dc4770e8c91 100644
--- a/frontend/common/types/requests.ts
+++ b/frontend/common/types/requests.ts
@@ -190,9 +190,9 @@ export type Req = {
projectId?: number
environmentId?: string
billing_period?:
- | 'current_billing_period'
- | 'previous_billing_period'
- | '90_day_period'
+ | 'current_billing_period'
+ | 'previous_billing_period'
+ | '90_day_period'
}
getWebhooks: {
environmentId: string
diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts
index de5a656f4504..7f3f5236c767 100644
--- a/frontend/common/types/responses.ts
+++ b/frontend/common/types/responses.ts
@@ -348,13 +348,13 @@ export type AuditLogItem = {
related_object_uuid?: number
related_feature_id?: number
related_object_type:
- | 'FEATURE'
- | 'FEATURE_STATE'
- | 'ENVIRONMENT'
- | 'CHANGE_REQUEST'
- | 'SEGMENT'
- | 'EF_VERSION'
- | 'EDGE_IDENTITY'
+ | 'FEATURE'
+ | 'FEATURE_STATE'
+ | 'ENVIRONMENT'
+ | 'CHANGE_REQUEST'
+ | 'SEGMENT'
+ | 'EF_VERSION'
+ | 'EDGE_IDENTITY'
is_system_event: boolean
}
diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js
index 64ea7f53fdfb..d681d59e13a3 100644
--- a/frontend/web/components/SegmentOverrides.js
+++ b/frontend/web/components/SegmentOverrides.js
@@ -454,7 +454,9 @@ class TheComponent extends Component {
const { highlightSegmentId, projectId, value } = this.props
if (!highlightSegmentId) return
- const existingOverride = value?.find((v) => v.segment === highlightSegmentId)
+ const existingOverride = value?.find(
+ (v) => v.segment === highlightSegmentId,
+ )
if (existingOverride) {
return
}
@@ -613,7 +615,7 @@ class TheComponent extends Component {