From 213580511ca59b7a9d66f14b26bb1e2cbafe3f66 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 3 Jan 2026 12:07:20 +0100 Subject: [PATCH] feature flags --- apps/website/.env.example | 24 ++- apps/website/README.md | 104 ++++++++++--- apps/website/env.d.ts | 1 + .../lib/feature/FeatureFlagService.test.ts | 144 ++++++++++++++++++ .../website/lib/feature/FeatureFlagService.ts | 42 ++++- 5 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 apps/website/lib/feature/FeatureFlagService.test.ts diff --git a/apps/website/.env.example b/apps/website/.env.example index 411d9d5bb..2a4974d01 100644 --- a/apps/website/.env.example +++ b/apps/website/.env.example @@ -1,12 +1,15 @@ # GridPilot Website Environment Variables # Application Mode -# Controls whether the site is in pre-launch or alpha mode -# Valid values: "pre-launch" | "alpha" -# Default: "pre-launch" (if not set) -# Note: NEXT_PUBLIC_ prefix exposes this to both server and browser +# pre-launch = landing page only, no features +# alpha = full platform with all features enabled automatically NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch +# Feature Flags (Optional - only needed for custom feature selection) +# When in alpha mode, all features are enabled automatically +# Use this to override or select specific features +# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature + # Vercel KV (for email signups and rate limiting) # Get these from: https://vercel.com/dashboard -> Storage -> KV # OPTIONAL in development (uses in-memory fallback) @@ -23,5 +26,14 @@ NEXT_PUBLIC_SITE_URL=https://gridpilot.com # Example: https://discord.gg/your-invite-code NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code -# Example for alpha mode: -# NEXT_PUBLIC_GRIDPILOT_MODE=alpha \ No newline at end of file +# Example configurations: + +# Pre-launch (default) +# NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch + +# Alpha with all features (recommended) +# NEXT_PUBLIC_GRIDPILOT_MODE=alpha + +# Alpha with specific features only +# NEXT_PUBLIC_GRIDPILOT_MODE=alpha +# FEATURE_FLAGS=driver_profiles,wallets \ No newline at end of file diff --git a/apps/website/README.md b/apps/website/README.md index f9ce34d78..f7658bcbe 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -58,7 +58,13 @@ Visit `http://localhost:3000` to see the landing page. | Variable | Description | Example | |----------|-------------|---------| -| `NEXT_PUBLIC_GRIDPILOT_MODE` | Application mode (server & client) | `pre-launch` or `alpha` | +| `NEXT_PUBLIC_GRIDPILOT_MODE` | Application mode (pre-launch or full platform) | `pre-launch` or `alpha` | + +### Optional Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `FEATURE_FLAGS` | Feature override (comma-separated) | `driver_profiles,wallets` | | `KV_REST_API_URL` | Vercel KV REST API endpoint | From Vercel Dashboard | | `KV_REST_API_TOKEN` | Vercel KV authentication token | From Vercel Dashboard | | `NEXT_PUBLIC_SITE_URL` | Public site URL | `https://gridpilot.com` | @@ -71,31 +77,95 @@ Visit `http://localhost:3000` to see the landing page. 4. Copy the `KV_REST_API_URL` and `KV_REST_API_TOKEN` from the database settings 5. Add them to your environment variables -## Mode Switching - -The application supports two modes: +## Application Modes ### Pre-Launch Mode (Default) - -- Shows landing page with email capture -- Only `/` and `/api/signup` routes are accessible -- All other routes return 404 - -To activate: ```bash NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch ``` +- Landing page only +- Email signup functionality +- No navigation or footer +- All platform features disabled -### Post-Launch Mode - -- Full platform access -- All routes are accessible -- Landing page is still available at `/` - -To activate: +### Alpha Mode (Full Platform) ```bash NEXT_PUBLIC_GRIDPILOT_MODE=alpha ``` +- Full platform access +- **All features automatically enabled** +- Navigation and footer visible +- Discovery section on landing page + +### Available Feature Flags +When in alpha mode, these features are automatically enabled: +- `driver_profiles` - Driver profile pages and features +- `team_profiles` - Team profile pages and features +- `wallets` - All wallet functionality (driver/team/league/sponsor) +- `sponsors` - Sponsor management features +- `team_feature` - Complete team functionality +- `alpha_features` - Landing page discovery section + +### Custom Feature Selection (Optional) +If you need fine-grained control, use `FEATURE_FLAGS`: +```bash +NEXT_PUBLIC_GRIDPILOT_MODE=alpha +FEATURE_FLAGS=driver_profiles,wallets +``` + +## Configuration Examples + +### Basic Alpha Setup (Recommended) +```bash +# .env.local +NEXT_PUBLIC_GRIDPILOT_MODE=alpha +# All features enabled automatically - no FEATURE_FLAGS needed +``` + +### Pre-Launch Setup +```bash +# .env.local +NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch +# Landing page only +``` + +### Custom Feature Selection +```bash +# .env.local +NEXT_PUBLIC_GRIDPILOT_MODE=alpha +FEATURE_FLAGS=driver_profiles,wallets +# Only driver profiles and wallets enabled +``` + +## How It Works + +1. **Mode Detection**: `NEXT_PUBLIC_GRIDPILOT_MODE` sets overall platform access +2. **Feature Bundling**: Alpha mode automatically enables all features via `FeatureFlagService` +3. **Override**: `FEATURE_FLAGS` can selectively enable/disable features +4. **Component Usage**: Components check feature flags using `FeatureFlagService.fromEnv().isEnabled('feature_name')` + +## Migration Guide + +### From Manual Feature Flags +**Before**: +```bash +NEXT_PUBLIC_GRIDPILOT_MODE=alpha +FEATURE_FLAGS=alpha_features,driver_profiles,team_profiles,wallets,sponsors,team_feature +``` + +**After**: +```bash +NEXT_PUBLIC_GRIDPILOT_MODE=alpha +# FEATURE_FLAGS not needed - everything automatic! +``` + +### No Code Changes Required +Your existing code continues to work: +```typescript +const featureService = FeatureFlagService.fromEnv(); +const isAlpha = featureService.isEnabled('alpha_features'); +// Works in both old and new system +``` ## Email Signup API diff --git a/apps/website/env.d.ts b/apps/website/env.d.ts index 726f3356e..20a8b0dcb 100644 --- a/apps/website/env.d.ts +++ b/apps/website/env.d.ts @@ -66,6 +66,7 @@ declare global { // Website (server-only) API_BASE_URL?: string; + FEATURE_FLAGS?: string; // Vercel KV (server-only) KV_REST_API_URL?: string; diff --git a/apps/website/lib/feature/FeatureFlagService.test.ts b/apps/website/lib/feature/FeatureFlagService.test.ts new file mode 100644 index 000000000..1946c060f --- /dev/null +++ b/apps/website/lib/feature/FeatureFlagService.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService'; + +describe('FeatureFlagService', () => { + describe('fromEnv() with alpha mode integration', () => { + let originalMode: string | undefined; + let originalFlags: string | undefined; + + beforeEach(() => { + originalMode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; + originalFlags = process.env.FEATURE_FLAGS; + }); + + afterEach(() => { + if (originalMode !== undefined) { + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = originalMode; + } else { + delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE; + } + + if (originalFlags !== undefined) { + process.env.FEATURE_FLAGS = originalFlags; + } else { + delete process.env.FEATURE_FLAGS; + } + }); + + it('should enable all features when NEXT_PUBLIC_GRIDPILOT_MODE is alpha', () => { + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha'; + + const service = FeatureFlagService.fromEnv(); + + expect(service.isEnabled('driver_profiles')).toBe(true); + expect(service.isEnabled('team_profiles')).toBe(true); + expect(service.isEnabled('wallets')).toBe(true); + expect(service.isEnabled('sponsors')).toBe(true); + expect(service.isEnabled('team_feature')).toBe(true); + expect(service.isEnabled('alpha_features')).toBe(true); + }); + + it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is pre-launch', () => { + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch'; + + const service = FeatureFlagService.fromEnv(); + + expect(service.isEnabled('driver_profiles')).toBe(false); + expect(service.isEnabled('team_profiles')).toBe(false); + expect(service.isEnabled('wallets')).toBe(false); + expect(service.isEnabled('sponsors')).toBe(false); + expect(service.isEnabled('team_feature')).toBe(false); + expect(service.isEnabled('alpha_features')).toBe(false); + }); + + it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => { + delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE; + + const service = FeatureFlagService.fromEnv(); + + expect(service.isEnabled('driver_profiles')).toBe(false); + expect(service.isEnabled('team_profiles')).toBe(false); + expect(service.isEnabled('wallets')).toBe(false); + expect(service.isEnabled('sponsors')).toBe(false); + expect(service.isEnabled('team_feature')).toBe(false); + expect(service.isEnabled('alpha_features')).toBe(false); + }); + + it('should allow FEATURE_FLAGS to override alpha mode', () => { + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha'; + process.env.FEATURE_FLAGS = 'driver_profiles,wallets'; + + const service = FeatureFlagService.fromEnv(); + + expect(service.isEnabled('driver_profiles')).toBe(true); + expect(service.isEnabled('wallets')).toBe(true); + expect(service.isEnabled('team_profiles')).toBe(false); + expect(service.isEnabled('sponsors')).toBe(false); + expect(service.isEnabled('team_feature')).toBe(false); + }); + + it('should return correct list of enabled flags in alpha mode', () => { + process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha'; + + const service = FeatureFlagService.fromEnv(); + const enabledFlags = service.getEnabledFlags(); + + expect(enabledFlags).toContain('driver_profiles'); + expect(enabledFlags).toContain('team_profiles'); + expect(enabledFlags).toContain('wallets'); + expect(enabledFlags).toContain('sponsors'); + expect(enabledFlags).toContain('team_feature'); + expect(enabledFlags).toContain('alpha_features'); + expect(enabledFlags.length).toBe(6); + }); + }); + + describe('Constructor behavior', () => { + it('should use provided flags array', () => { + const service = new FeatureFlagService(['test_flag']); + expect(service.isEnabled('test_flag')).toBe(true); + expect(service.isEnabled('other_flag')).toBe(false); + }); + + it('should parse FEATURE_FLAGS environment variable', () => { + process.env.FEATURE_FLAGS = 'flag1, flag2, flag3'; + const service = new FeatureFlagService(); + + expect(service.isEnabled('flag1')).toBe(true); + expect(service.isEnabled('flag2')).toBe(true); + expect(service.isEnabled('flag3')).toBe(true); + expect(service.isEnabled('flag4')).toBe(false); + + delete process.env.FEATURE_FLAGS; + }); + + it('should handle empty FEATURE_FLAGS', () => { + process.env.FEATURE_FLAGS = ''; + const service = new FeatureFlagService(); + + expect(service.isEnabled('any_flag')).toBe(false); + expect(service.getEnabledFlags()).toEqual([]); + + delete process.env.FEATURE_FLAGS; + }); + }); +}); + +describe('MockFeatureFlagService', () => { + it('should work with provided flags', () => { + const service = new MockFeatureFlagService(['test_flag']); + expect(service.isEnabled('test_flag')).toBe(true); + expect(service.isEnabled('other_flag')).toBe(false); + }); + + it('should return empty array when no flags provided', () => { + const service = new MockFeatureFlagService(); + expect(service.getEnabledFlags()).toEqual([]); + }); +}); + +describe('mockFeatureFlags default instance', () => { + it('should have alpha_features enabled by default', () => { + expect(mockFeatureFlags.isEnabled('alpha_features')).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/website/lib/feature/FeatureFlagService.ts b/apps/website/lib/feature/FeatureFlagService.ts index d5e737646..65db429ba 100644 --- a/apps/website/lib/feature/FeatureFlagService.ts +++ b/apps/website/lib/feature/FeatureFlagService.ts @@ -1,7 +1,12 @@ /** * FeatureFlagService - Manages feature flags for both server and client - * + * + * Automatic Alpha Mode Integration: + * When NEXT_PUBLIC_GRIDPILOT_MODE=alpha, all features are automatically enabled. + * This eliminates the need to manually set FEATURE_FLAGS for alpha deployments. + * * Server: Reads from process.env.FEATURE_FLAGS (comma-separated) + * OR auto-enables all features if in alpha mode * Client: Reads from session context or provides mock implementation */ @@ -15,7 +20,7 @@ export class FeatureFlagService { } else { // Parse from environment variable const flagsEnv = process.env.FEATURE_FLAGS; - this.flags = flagsEnv + this.flags = flagsEnv ? new Set(flagsEnv.split(',').map(f => f.trim())) : new Set(); } @@ -37,8 +42,31 @@ export class FeatureFlagService { /** * Factory method to create service with environment flags + * Automatically enables all features if in alpha mode + * FEATURE_FLAGS can override alpha mode defaults */ static fromEnv(): FeatureFlagService { + const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; + const flagsEnv = process.env.FEATURE_FLAGS; + + // If FEATURE_FLAGS is explicitly set, use it (overrides alpha mode) + if (flagsEnv) { + return new FeatureFlagService(); + } + + // If in alpha mode, automatically enable all features + if (mode === 'alpha') { + return new FeatureFlagService([ + 'driver_profiles', + 'team_profiles', + 'wallets', + 'sponsors', + 'team_feature', + 'alpha_features' + ]); + } + + // Otherwise, use FEATURE_FLAGS environment variable (empty if not set) return new FeatureFlagService(); } } @@ -67,4 +95,12 @@ export class MockFeatureFlagService implements FeatureFlagContextType { } // Default mock instance for client-side usage -export const mockFeatureFlags = new MockFeatureFlagService(['alpha_features']); \ No newline at end of file +// Enables all features for development/demo mode +export const mockFeatureFlags = new MockFeatureFlagService([ + 'driver_profiles', + 'team_profiles', + 'wallets', + 'sponsors', + 'team_feature', + 'alpha_features' +]); \ No newline at end of file