/** biome-ignore-all lint/correctness/noUnusedVariables: allowed in this store */ import type { StoreApi, UseBoundStore } from 'zustand' import { create } from 'zustand' import { persist } from 'zustand/middleware' import { allLanguages } from '../../providers/I18nProvider/constants.js' import { loadLocale } from '../../providers/I18nProvider/i18n.js' import type { LanguageKey } from '../../providers/I18nProvider/types.js' import type { WidgetConfig } from '../../types/widget.js' import { getConfigItemSets, isItemAllowedForSets } from '../../utils/item.js' import type { SettingsProps, SettingsState, SettingsToolType } from './types.js' import { SettingsToolTypes } from './types.js' import { getStateValues } from './utils/getStateValues.js' export const defaultSlippage = undefined export const defaultConfigurableSettings: Pick< SettingsState, 'routePriority' | 'slippage' | 'gasPrice' > = { routePriority: 'CHEAPEST', slippage: defaultSlippage, gasPrice: 'normal', } const defaultSettings: SettingsProps = { gasPrice: 'normal', enabledAutoRefuel: true, disabledBridges: [], disabledExchanges: [], enabledBridges: [], enabledExchanges: [], _enabledBridges: {}, _enabledExchanges: {}, smallBalanceThreshold: undefined, } export const createSettingsStore = ( config: WidgetConfig ): UseBoundStore> => { const configItemsByToolType = { Bridges: config.bridges, Exchanges: config.exchanges, } // Tool state from a candidate map, keeping only config-allowed tools. const buildToolState = ( toolType: SettingsToolType, enabledMap: Record ) => { const configItemSets = getConfigItemSets( configItemsByToolType[toolType], (items) => new Set(items) ) const allowed: Record = Object.fromEntries( Object.entries(enabledMap).filter(([key]) => isItemAllowedForSets(key, configItemSets) ) ) const keys = Object.keys(allowed) return { [`_enabled${toolType}`]: allowed, [`enabled${toolType}`]: keys.filter((key) => allowed[key]), [`disabled${toolType}`]: keys.filter((key) => !allowed[key]), } } const toEnabledMap = (keys: string[] = []): Record => Object.fromEntries(keys.map((key) => [key, true] as const)) // Deny-only configs can't be seeded; the tool universe is unknown until /tools. const initialSettings: SettingsProps = { ...defaultSettings, ...buildToolState('Bridges', toEnabledMap(config.bridges?.allow)), ...buildToolState('Exchanges', toEnabledMap(config.exchanges?.allow)), } return create()( persist( (set, get) => ({ ...initialSettings, setValue: (key, value) => set(() => ({ [key]: value, })), setValues: (values: Partial) => set(() => ({ ...values, })), getSettings: () => getStateValues(get()), getValue: (key) => get()[key], initializeTools: (toolType, tools, reset) => { if (!tools.length) { return } set((state) => { const updatedState = { ...state } if (!reset) { // Add new tools tools.forEach((tool) => { if (!Object.hasOwn(updatedState[`_enabled${toolType}`], tool)) { updatedState[`_enabled${toolType}`][tool] = true } }) // Filter tools we no longer have updatedState[`_enabled${toolType}`] = Object.fromEntries( Object.entries(updatedState[`_enabled${toolType}`]).filter( ([key]) => tools.includes(key) ) ) } else { tools.forEach((tool) => { updatedState[`_enabled${toolType}`][tool] = true }) } const enabledToolKeys = Object.keys( updatedState[`_enabled${toolType}`] ) updatedState[`enabled${toolType}`] = enabledToolKeys.filter( (key) => updatedState[`_enabled${toolType}`][key] ) updatedState[`disabled${toolType}`] = enabledToolKeys.filter( (key) => !updatedState[`_enabled${toolType}`][key] ) return updatedState }) }, setToolValue: (toolType, tool, value) => set((state) => { const enabledTools = { ...state[`_enabled${toolType}`], [tool]: value, } const enabledToolKeys = Object.keys(enabledTools) return { [`_enabled${toolType}`]: enabledTools, [`enabled${toolType}`]: enabledToolKeys.filter( (key) => enabledTools[key] ), [`disabled${toolType}`]: enabledToolKeys.filter( (key) => !enabledTools[key] ), } }), toggleToolKeys: (toolType, toolKeys) => set((state) => { const allKeysInCollectionEnabled = toolKeys.every( (toolKey) => state[`_enabled${toolType}`][toolKey] ) // then toggle those keys to false const updatedTools = toolKeys.reduce( (accum, toolKey) => { accum[toolKey] = !allKeysInCollectionEnabled return accum }, { ...state[`_enabled${toolType}`], } ) const enableKeys: string[] = [] const disabledKeys: string[] = [] Object.entries(updatedTools).forEach(([key, value]) => { if (value) { enableKeys.push(key) } else { disabledKeys.push(key) } }) return { [`_enabled${toolType}`]: updatedTools, [`enabled${toolType}`]: enableKeys, [`disabled${toolType}`]: disabledKeys, } }), reset: (bridges, exchanges) => { set(() => ({ ...defaultSettings, ...defaultConfigurableSettings, })) get().initializeTools('Bridges', bridges, true) get().initializeTools('Exchanges', exchanges, true) return { ...get() } }, }), { name: 'li.fi-widget-settings', version: 6, partialize: (state) => { const { disabledBridges, disabledExchanges, enabledBridges, enabledExchanges, ...partializedState } = state return partializedState }, merge: (persistedState: any, currentState: SettingsState) => { const state = { ...currentState, ...persistedState } SettingsToolTypes.forEach((toolType) => { const persistedEnabled = persistedState?.[`_enabled${toolType}`] if (persistedEnabled) { // Overlay persisted toggles on the config allow seed so an empty // intersection keeps the allow-list instead of going unrestricted. const allowMap = toEnabledMap( configItemsByToolType[toolType]?.allow ) Object.assign( state, buildToolState(toolType, { ...allowMap, ...persistedEnabled }) ) } }) return state }, migrate: () => ({}), onRehydrateStorage: () => { const initializeLanguageSettings = async ( state: SettingsState, initialLanguage?: string ) => { const customLanguages = Object.keys( config?.languageResources || {} ).filter((key) => !allLanguages.includes(key as LanguageKey)) if ( initialLanguage && // Custom language resources and English are added statically. initialLanguage !== 'en' && !customLanguages.includes(initialLanguage) ) { await loadLocale( initialLanguage as LanguageKey, config?.languageResources?.[initialLanguage as LanguageKey] ).then((languageResource) => { state.setValues({ language: initialLanguage, languageCache: languageResource, }) }) } } return (state?: SettingsState) => { // Preload translations (from existing translation files) if (state) { // NB: State always exists unless there is any error initializeLanguageSettings( state, state.getValue('language') || config?.languages?.default ) } } }, } ) ) }