refactor: remove extension toggle system — compiled-in extensions are always active (#59)

The runtime toggle system (extension_toggles table, API routes, hooks, UI components)
added unnecessary complexity. Extensions controlled via extensions.config.json at build
time are now always active for all users. This removes ~835 lines of toggle-related code
including API routes, DB queries, the ExtensionToggleButton component, useEnabledExtensions
and useExtensionToggle hooks, and the toggle-check module. AI consent gating remains
unchanged.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-20 09:31:29 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 38b658205d
commit cf77adaa0a
30 changed files with 115 additions and 833 deletions
@@ -1,48 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createMockSupabase } from '@/tests/helpers'
const { supabase: mockSupabase, mockResult } = createMockSupabase()
vi.mock('@/lib/supabase/server', () => ({
createServiceClient: () => Promise.resolve(mockSupabase),
}))
import { isExtensionEnabled } from '../toggle-check'
describe('isExtensionEnabled', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns true when toggle exists and is enabled', async () => {
mockResult({ data: { enabled: true }, error: null })
const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr')
expect(result).toBe(true)
expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles')
})
it('returns false when toggle exists and is disabled', async () => {
mockResult({ data: { enabled: false }, error: null })
const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr')
expect(result).toBe(false)
})
it('returns true for legacy general extensions when no toggle row exists', async () => {
mockResult({ data: null, error: null })
const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr')
expect(result).toBe(true)
})
it('returns false for non-legacy extensions when no toggle row exists', async () => {
mockResult({ data: null, error: null })
const result = await isExtensionEnabled('user-1', 'restaurant', 'tip-tracking')
expect(result).toBe(false)
})
})
-77
View File
@@ -1,77 +0,0 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import type { ExtensionToggle } from './types'
export function useEnabledExtensions() {
const [extensions, setExtensions] = useState<ExtensionToggle[]>([])
const [isLoading, setIsLoading] = useState(true)
const refresh = useCallback(async () => {
setIsLoading(true)
try {
const res = await fetch('/api/extensions/toggles')
if (res.ok) {
const { data } = await res.json()
setExtensions(data ?? [])
}
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
refresh()
}, [refresh])
return { extensions, isLoading, refresh }
}
export function useExtensionToggle(sectorSlug: string, extensionSlug: string) {
const [enabled, setEnabled] = useState(false)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const check = async () => {
setIsLoading(true)
try {
const res = await fetch(`/api/extensions/toggles/${sectorSlug}/${extensionSlug}`)
if (res.ok) {
const { data } = await res.json()
setEnabled(data?.enabled ?? false)
}
} finally {
setIsLoading(false)
}
}
check()
}, [sectorSlug, extensionSlug])
const toggle = useCallback(async () => {
const newValue = !enabled
setEnabled(newValue) // Optimistic update
try {
const res = await fetch('/api/extensions/toggles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sector_slug: sectorSlug,
extension_slug: extensionSlug,
enabled: newValue,
}),
})
if (!res.ok) {
setEnabled(!newValue) // Revert on error
} else {
// Notify other components about the toggle change
window.dispatchEvent(new CustomEvent('extension-toggle-changed', {
detail: { sectorSlug, extensionSlug, enabled: newValue },
}))
}
} catch {
setEnabled(!newValue) // Revert on error
}
}, [enabled, sectorSlug, extensionSlug])
return { enabled, isLoading, toggle }
}
+1 -6
View File
@@ -2,14 +2,9 @@ export { extensionRegistry } from './registry'
export { loadExtensions } from './loader'
export type {
Extension,
RouteDefinition,
ApiRouteDefinition,
SidebarItem,
ReportDefinition,
SettingsPanelDefinition,
TaxCodeDefinition,
DimensionDefinition,
MappingRuleTypeDefinition,
ExtensionEventHandler,
ExtensionContext,
SettingsPanelDefinition,
} from './types'
+3 -12
View File
@@ -48,21 +48,12 @@ export function getExtensionsBySector(slug: SectorSlug): ExtensionDefinition[] {
return getSector(slug)?.extensions ?? []
}
/** Extensions with a workspace and a quickAction href — for sidebar nav.
* Filters against the user's enabled extensions when provided. */
export function getExtensionNavItems(
enabledExtensions?: { sector_slug: string; extension_slug: string }[]
): { href: string; label: string; icon: string }[] {
/** Extensions with a workspace and a quickAction href — for sidebar nav. */
export function getExtensionNavItems(): { href: string; label: string; icon: string }[] {
return getAllExtensions()
.filter(e => {
const key = `${e.sector}/${e.slug}`
if (!(key in WORKSPACES) || !e.quickAction?.href) return false
if (enabledExtensions) {
return enabledExtensions.some(
t => t.sector_slug === e.sector && t.extension_slug === e.slug
)
}
return true
return key in WORKSPACES && !!e.quickAction?.href
})
.sort((a, b) => (a.quickAction!.order ?? 0) - (b.quickAction!.order ?? 0))
.map(e => ({
-43
View File
@@ -1,43 +0,0 @@
import { createServiceClient } from '@/lib/supabase/server'
/**
* Check if an extension is enabled for a specific user.
* Used by event handlers to gate execution.
*
* For backward compatibility during the transition period,
* general extensions that were previously always-on default to
* enabled when no toggle row exists.
*/
export const LEGACY_GENERAL_EXTENSIONS = [
'receipt-ocr',
'ai-categorization',
'ai-chat',
'push-notifications',
'enable-banking',
'email',
'arcim-migration',
'tic',
]
export async function isExtensionEnabled(
userId: string,
sectorSlug: string,
extensionSlug: string
): Promise<boolean> {
const supabase = await createServiceClient()
const { data } = await supabase
.from('extension_toggles')
.select('enabled')
.eq('user_id', userId)
.eq('sector_slug', sectorSlug)
.eq('extension_slug', extensionSlug)
.single()
// If no toggle row exists, check if this is a legacy extension
if (!data) {
return sectorSlug === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(extensionSlug)
}
return data.enabled
}
-11
View File
@@ -52,17 +52,6 @@ export interface Sector {
extensions: ExtensionDefinition[]
}
/** Database row for extension toggle state */
export interface ExtensionToggle {
id: string
user_id: string
sector_slug: string
extension_slug: string
enabled: boolean
created_at: string
updated_at: string
}
// ============================================================
// Extension Interface & Supporting Types
// ============================================================