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:
co-authored by
Claude Opus 4.6
parent
38b658205d
commit
cf77adaa0a
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user