From cf48cf22bec198099284cf3ce6ed6609b76a5f39 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Thu, 17 Oct 2024 14:36:18 +0200 Subject: [PATCH] feat: emit events for settings changes (#312) Co-authored-by: Eugene Chybisov --- .../GasMessage/GasRefuelMessage.tsx | 5 +- packages/widget/src/hooks/useLanguages.ts | 4 +- .../widget/src/hooks/useSettingMonitor.ts | 4 +- .../src/pages/SelectEnabledToolsPage.tsx | 16 +- .../pages/SettingsPage/GasPriceSettings.tsx | 4 +- .../SettingsPage/RoutePrioritySettings.tsx | 4 +- .../SlippageSettings/SlippageSettings.tsx | 52 +++--- .../WidgetProvider/WidgetProvider.tsx | 5 +- packages/widget/src/stores/settings/types.ts | 9 +- .../src/stores/settings/useAppearance.ts | 24 ++- .../src/stores/settings/useSettingsActions.ts | 150 ++++++++++++++++++ .../src/stores/settings/useSettingsStore.ts | 38 +---- .../stores/settings/utils/getStateValues.ts | 16 ++ packages/widget/src/types/events.ts | 13 ++ packages/widget/src/utils/deepEqual.ts | 62 ++++++++ 15 files changed, 314 insertions(+), 92 deletions(-) create mode 100644 packages/widget/src/stores/settings/useSettingsActions.ts create mode 100644 packages/widget/src/stores/settings/utils/getStateValues.ts create mode 100644 packages/widget/src/utils/deepEqual.ts diff --git a/packages/widget/src/components/GasMessage/GasRefuelMessage.tsx b/packages/widget/src/components/GasMessage/GasRefuelMessage.tsx index 60acafc6b..009c0c994 100644 --- a/packages/widget/src/components/GasMessage/GasRefuelMessage.tsx +++ b/packages/widget/src/components/GasMessage/GasRefuelMessage.tsx @@ -5,13 +5,14 @@ import type { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useGasRefuel } from '../../hooks/useGasRefuel.js'; import { useSettings } from '../../stores/settings/useSettings.js'; -import { useSettingsStore } from '../../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../../stores/settings/useSettingsActions.js'; import { AlertMessage } from '../AlertMessage/AlertMessage.js'; import { InfoMessageSwitch } from './GasMessage.style.js'; export const GasRefuelMessage: React.FC = (props) => { const { t } = useTranslation(); - const setValue = useSettingsStore((state) => state.setValue); + + const { setValue } = useSettingsActions(); const { enabledAutoRefuel } = useSettings(['enabledAutoRefuel']); const { enabled, chain, isLoading: isRefuelLoading } = useGasRefuel(); diff --git a/packages/widget/src/hooks/useLanguages.ts b/packages/widget/src/hooks/useLanguages.ts index a043d3523..46bff4934 100644 --- a/packages/widget/src/hooks/useLanguages.ts +++ b/packages/widget/src/hooks/useLanguages.ts @@ -1,13 +1,13 @@ import { useTranslation } from 'react-i18next'; import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'; import { useSettings } from '../stores/settings/useSettings.js'; -import { useSettingsStore } from '../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../stores/settings/useSettingsActions.js'; export const useLanguages = () => { const { t, i18n } = useTranslation(); const { languages } = useWidgetConfig(); const { language } = useSettings(['language']); - const setValue = useSettingsStore((state) => state.setValue); + const { setValue } = useSettingsActions(); const sortedLanguages = Object.keys(i18n.store.data).sort(); diff --git a/packages/widget/src/hooks/useSettingMonitor.ts b/packages/widget/src/hooks/useSettingMonitor.ts index 6e0aedcc8..bf639f097 100644 --- a/packages/widget/src/hooks/useSettingMonitor.ts +++ b/packages/widget/src/hooks/useSettingMonitor.ts @@ -1,8 +1,8 @@ import { shallow } from 'zustand/shallow'; import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'; +import { useSettingsActions } from '../stores/settings/useSettingsActions.js'; import { defaultConfigurableSettings, - setDefaultSettings, useSettingsStore, } from '../stores/settings/useSettingsStore.js'; import { useTools } from './useTools.js'; @@ -25,8 +25,8 @@ export const useSettingMonitor = () => { shallow, ); const { tools } = useTools(); - const resetSettings = useSettingsStore((state) => state.reset); const config = useWidgetConfig(); + const { setDefaultSettings, resetSettings } = useSettingsActions(); const isSlippageChanged = config.slippage ? Number(slippage) !== config.slippage * 100 diff --git a/packages/widget/src/pages/SelectEnabledToolsPage.tsx b/packages/widget/src/pages/SelectEnabledToolsPage.tsx index 69c2a94c3..4c054449e 100644 --- a/packages/widget/src/pages/SelectEnabledToolsPage.tsx +++ b/packages/widget/src/pages/SelectEnabledToolsPage.tsx @@ -27,6 +27,7 @@ import { useDefaultElementId } from '../hooks/useDefaultElementId.js'; import { useHeader } from '../hooks/useHeader.js'; import { useScrollableContainer } from '../hooks/useScrollableContainer.js'; import { useTools } from '../hooks/useTools.js'; +import { useSettingsActions } from '../stores/settings/useSettingsActions.js'; import { useSettingsStore } from '../stores/settings/useSettingsStore.js'; interface SelectAllCheckboxProps { @@ -78,16 +79,11 @@ export const SelectEnabledToolsPage: React.FC<{ }> = ({ type }) => { const typeKey = type.toLowerCase() as 'bridges' | 'exchanges'; const { tools } = useTools(); - const [enabledTools, disabledTools, setToolValue, toggleToolKeys] = - useSettingsStore( - (state) => [ - state[`_enabled${type}`], - state[`disabled${type}`], - state.setToolValue, - state.toggleToolKeys, - ], - shallow, - ); + const { setToolValue, toggleToolKeys } = useSettingsActions(); + const [enabledTools, disabledTools] = useSettingsStore( + (state) => [state[`_enabled${type}`], state[`disabled${type}`]], + shallow, + ); const { t } = useTranslation(); const elementId = useDefaultElementId(); diff --git a/packages/widget/src/pages/SettingsPage/GasPriceSettings.tsx b/packages/widget/src/pages/SettingsPage/GasPriceSettings.tsx index a61b79965..adecf0eea 100644 --- a/packages/widget/src/pages/SettingsPage/GasPriceSettings.tsx +++ b/packages/widget/src/pages/SettingsPage/GasPriceSettings.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next'; import { CardTabs, Tab } from '../../components/Tabs/Tabs.style.js'; import { useSettingMonitor } from '../../hooks/useSettingMonitor.js'; import { useSettings } from '../../stores/settings/useSettings.js'; -import { useSettingsStore } from '../../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../../stores/settings/useSettingsActions.js'; import { BadgedValue } from './SettingsCard/BadgedValue.js'; import { SettingCardExpandable } from './SettingsCard/SettingCardExpandable.js'; export const GasPriceSettings: React.FC = () => { const { t } = useTranslation(); - const setValue = useSettingsStore((state) => state.setValue); + const { setValue } = useSettingsActions(); const { isGasPriceChanged } = useSettingMonitor(); const { gasPrice } = useSettings(['gasPrice']); diff --git a/packages/widget/src/pages/SettingsPage/RoutePrioritySettings.tsx b/packages/widget/src/pages/SettingsPage/RoutePrioritySettings.tsx index 41c681af2..6d57e43af 100644 --- a/packages/widget/src/pages/SettingsPage/RoutePrioritySettings.tsx +++ b/packages/widget/src/pages/SettingsPage/RoutePrioritySettings.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { CardTabs, Tab } from '../../components/Tabs/Tabs.style.js'; import { useSettingMonitor } from '../../hooks/useSettingMonitor.js'; import { useSettings } from '../../stores/settings/useSettings.js'; -import { useSettingsStore } from '../../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../../stores/settings/useSettingsActions.js'; import { BadgedValue } from './SettingsCard/BadgedValue.js'; import { SettingCardExpandable } from './SettingsCard/SettingCardExpandable.js'; @@ -12,7 +12,7 @@ const Priorities: Order[] = ['CHEAPEST', 'FASTEST']; export const RoutePrioritySettings: React.FC = () => { const { t } = useTranslation(); - const setValue = useSettingsStore((state) => state.setValue); + const { setValue } = useSettingsActions(); const { isRoutePriorityChanged } = useSettingMonitor(); const { routePriority } = useSettings(['routePriority']); const currentRoutePriority = routePriority ?? ''; diff --git a/packages/widget/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx b/packages/widget/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx index cddf1f039..b33b049f5 100644 --- a/packages/widget/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx +++ b/packages/widget/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx @@ -1,14 +1,12 @@ import { Percent, WarningRounded } from '@mui/icons-material'; -import { Box, Typography } from '@mui/material'; +import { Box, Typography, debounce } from '@mui/material'; import type { ChangeEventHandler, FocusEventHandler } from 'react'; -import { useRef, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSettingMonitor } from '../../../hooks/useSettingMonitor.js'; import { useSettings } from '../../../stores/settings/useSettings.js'; -import { - defaultSlippage, - useSettingsStore, -} from '../../../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../../../stores/settings/useSettingsActions.js'; +import { defaultSlippage } from '../../../stores/settings/useSettingsStore.js'; import { formatSlippage } from '../../../utils/format.js'; import { BadgedValue } from '../SettingsCard/BadgedValue.js'; import { SettingCardExpandable } from '../SettingsCard/SettingCardExpandable.js'; @@ -24,36 +22,48 @@ export const SlippageSettings: React.FC = () => { const { isSlippageOutsideRecommendedLimits, isSlippageChanged } = useSettingMonitor(); const { slippage } = useSettings(['slippage']); - const setValue = useSettingsStore((state) => state.setValue); + const { setValue } = useSettingsActions(); const defaultValue = useRef(slippage); const [focused, setFocused] = useState<'input' | 'button'>(); + const customInputValue = + !slippage || slippage === defaultSlippage ? '' : slippage; + + const [inputValue, setInputValue] = useState(customInputValue); + const handleDefaultClick = () => { setValue('slippage', formatSlippage(defaultSlippage, defaultValue.current)); }; - const handleInputUpdate: ChangeEventHandler = (event) => { - const { value } = event.target; + const debouncedSetValue = useMemo(() => debounce(setValue, 500), [setValue]); - setValue( - 'slippage', - formatSlippage(value || defaultSlippage, defaultValue.current, true), - ); - }; + const handleInputUpdate: ChangeEventHandler = useCallback( + (event) => { + const { value } = event.target; + + setInputValue(formatSlippage(value, defaultValue.current, true)); + + debouncedSetValue( + 'slippage', + formatSlippage(value || defaultSlippage, defaultValue.current, true), + ); + }, + [debouncedSetValue], + ); const handleInputBlur: FocusEventHandler = (event) => { setFocused(undefined); const { value } = event.target; - setValue( - 'slippage', - formatSlippage(value || defaultSlippage, defaultValue.current), + const formattedValue = formatSlippage( + value || defaultSlippage, + defaultValue.current, ); - }; + setInputValue(formattedValue === defaultSlippage ? '' : formattedValue); - const customInputValue = - !slippage || slippage === defaultSlippage ? '' : slippage; + setValue('slippage', formattedValue); + }; const badgeColor = isSlippageOutsideRecommendedLimits ? 'warning' @@ -98,7 +108,7 @@ export const SlippageSettings: React.FC = () => { setFocused('input'); }} onBlur={handleInputBlur} - value={customInputValue} + value={inputValue} autoComplete="off" /> diff --git a/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx b/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx index 808dfeb94..68b8b47ec 100644 --- a/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx +++ b/packages/widget/src/providers/WidgetProvider/WidgetProvider.tsx @@ -1,7 +1,7 @@ import { config, createConfig, type SDKConfig } from '@lifi/sdk'; import { createContext, useContext, useId, useMemo } from 'react'; import { version } from '../../config/version.js'; -import { setDefaultSettings } from '../../stores/settings/useSettingsStore.js'; +import { useSettingsActions } from '../../stores/settings/useSettingsActions.js'; import type { WidgetContextProps, WidgetProviderProps } from './types.js'; const initialContext: WidgetContextProps = { @@ -20,6 +20,7 @@ export const WidgetProvider: React.FC< React.PropsWithChildren > = ({ children, config: widgetConfig }) => { const elementId = useId(); + const { setDefaultSettings } = useSettingsActions(); if (!widgetConfig?.integrator) { throw Error('Required property "integrator" is missing.'); @@ -68,7 +69,7 @@ export const WidgetProvider: React.FC< integrator: widgetConfig.integrator, }; } - }, [elementId, widgetConfig]); + }, [elementId, widgetConfig, setDefaultSettings]); return ( {children} ); diff --git a/packages/widget/src/stores/settings/types.ts b/packages/widget/src/stores/settings/types.ts index cf9d83e2d..b7530573a 100644 --- a/packages/widget/src/stores/settings/types.ts +++ b/packages/widget/src/stores/settings/types.ts @@ -9,6 +9,8 @@ export type ValueSetter = ( value: S[Extract], ) => void; +export type ValueGetter = (key: K) => S[K]; + export type ValuesSetter = ( values: Record]>, ) => void; @@ -31,9 +33,10 @@ export interface SettingsProps { _enabledExchanges: Record; } -export interface SettingsState extends SettingsProps { +export interface SettingsActions { setValue: ValueSetter; - setValues: ValuesSetter; + getValue: ValueGetter; + getSettings: () => SettingsProps; initializeTools( toolType: SettingsToolType, tools: string[], @@ -44,6 +47,8 @@ export interface SettingsState extends SettingsProps { reset(bridges: string[], exchanges: string[]): void; } +export type SettingsState = SettingsProps & SettingsActions; + export interface SendToWalletState { showSendToWallet: boolean; } diff --git a/packages/widget/src/stores/settings/useAppearance.ts b/packages/widget/src/stores/settings/useAppearance.ts index 155c17ded..8d6660f2b 100644 --- a/packages/widget/src/stores/settings/useAppearance.ts +++ b/packages/widget/src/stores/settings/useAppearance.ts @@ -1,17 +1,15 @@ -import { shallow } from 'zustand/shallow'; -import type { Appearance } from '../../types/widget.js'; -import { useSettingsStore } from './useSettingsStore.js'; +import { useSettingsActions } from "../../stores/settings/useSettingsActions.js"; +import type { Appearance } from "../../types/widget.js"; +import { useSettingsStore } from "./useSettingsStore.js"; export const useAppearance = (): [ - Appearance, - (appearance: Appearance) => void, + Appearance, + (appearance: Appearance) => void, ] => { - const [appearance, setValue] = useSettingsStore( - (state) => [state.appearance, state.setValue], - shallow, - ); - const setAppearance = (appearance: Appearance) => { - setValue('appearance', appearance); - }; - return [appearance, setAppearance]; + const { setValue } = useSettingsActions(); + const appearance = useSettingsStore((state) => state.appearance); + const setAppearance = (appearance: Appearance) => { + setValue("appearance", appearance); + }; + return [appearance, setAppearance]; }; diff --git a/packages/widget/src/stores/settings/useSettingsActions.ts b/packages/widget/src/stores/settings/useSettingsActions.ts new file mode 100644 index 000000000..56156585f --- /dev/null +++ b/packages/widget/src/stores/settings/useSettingsActions.ts @@ -0,0 +1,150 @@ +import { useCallback } from 'react'; +import { shallow } from 'zustand/shallow'; +import type { widgetEvents } from '../../hooks/useWidgetEvents.js'; +import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'; +import { WidgetEvent } from '../../types/events.js'; +import type { WidgetConfig } from '../../types/widget.js'; +import { deepEqual } from '../../utils/deepEqual.js'; +import type { + SettingsActions, + SettingsProps, + SettingsToolType, + ValueSetter, +} from './types.js'; +import { + defaultConfigurableSettings, + useSettingsStore, +} from './useSettingsStore.js'; + +const emitEventOnChange = any>( + emitter: typeof widgetEvents, + actions: Omit, + settingFunction: T, + ...args: Parameters +) => { + const oldSettings = actions.getSettings(); + + settingFunction(...args); + + const newSettings = actions.getSettings(); + + if (!deepEqual(oldSettings, newSettings)) { + (Object.keys(oldSettings) as (keyof SettingsProps)[]).forEach((toolKey) => { + if (!deepEqual(oldSettings[toolKey], newSettings[toolKey])) { + emitter.emit(WidgetEvent.SettingUpdated, { + setting: toolKey, + newValue: newSettings[toolKey], + oldValue: oldSettings[toolKey], + newSettings: newSettings, + oldSettings: oldSettings, + }); + } + }); + } +}; + +export const useSettingsActions = () => { + const emitter = useWidgetEvents(); + const actions = useSettingsStore( + (state) => ({ + setValue: state.setValue, + getValue: state.getValue, + getSettings: state.getSettings, + reset: state.reset, + setToolValue: state.setToolValue, + toggleToolKeys: state.toggleToolKeys, + }), + shallow, + ); + + const setValueWithEmittedEvent = useCallback>( + (value, newValue) => { + const setting = value as keyof SettingsProps; + emitEventOnChange(emitter, actions, actions.setValue, setting, newValue); + }, + [emitter, actions], + ); + + const setDefaultSettingsWithEmittedEvents = useCallback( + (config?: WidgetConfig) => { + const slippage = actions.getValue('slippage'); + const routePriority = actions.getValue('routePriority'); + const gasPrice = actions.getValue('gasPrice'); + + const defaultSlippage = + (config?.slippage || config?.sdkConfig?.routeOptions?.slippage || 0) * + 100; + const defaultRoutePriority = + config?.routePriority || config?.sdkConfig?.routeOptions?.order; + + defaultConfigurableSettings.slippage = ( + defaultSlippage || defaultConfigurableSettings.slippage + )?.toString(); + + defaultConfigurableSettings.routePriority = + defaultRoutePriority || defaultConfigurableSettings.routePriority; + + if (!slippage) { + setValueWithEmittedEvent( + 'slippage', + defaultConfigurableSettings.slippage, + ); + } + if (!routePriority) { + setValueWithEmittedEvent( + 'routePriority', + defaultConfigurableSettings.routePriority, + ); + } + if (!gasPrice) { + setValueWithEmittedEvent( + 'gasPrice', + defaultConfigurableSettings.gasPrice, + ); + } + }, + [actions, setValueWithEmittedEvent], + ); + + const resetWithEmittedEvents = useCallback( + (bridges: string[], exchanges: string[]) => { + emitEventOnChange(emitter, actions, actions.reset, bridges, exchanges); + }, + [emitter, actions], + ); + + const setToolValueWithEmittedEvents = useCallback( + (toolType: SettingsToolType, tool: string, value: boolean) => { + emitEventOnChange( + emitter, + actions, + actions.setToolValue, + toolType, + tool, + value, + ); + }, + [emitter, actions], + ); + + const toggleToolKeysWithEmittedEvents = useCallback( + (toolType: SettingsToolType, toolKeys: string[]) => { + emitEventOnChange( + emitter, + actions, + actions.toggleToolKeys, + toolType, + toolKeys, + ); + }, + [emitter, actions], + ); + + return { + setValue: setValueWithEmittedEvent, + setDefaultSettings: setDefaultSettingsWithEmittedEvents, + resetSettings: resetWithEmittedEvents, + setToolValue: setToolValueWithEmittedEvents, + toggleToolKeys: toggleToolKeysWithEmittedEvents, + }; +}; diff --git a/packages/widget/src/stores/settings/useSettingsStore.ts b/packages/widget/src/stores/settings/useSettingsStore.ts index a7b8fe3cf..0e83a3844 100644 --- a/packages/widget/src/stores/settings/useSettingsStore.ts +++ b/packages/widget/src/stores/settings/useSettingsStore.ts @@ -1,9 +1,9 @@ import type { StateCreator } from 'zustand'; import { persist } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; -import type { WidgetConfig } from '../../types/widget.js'; import type { SettingsProps, SettingsState } from './types.js'; import { SettingsToolTypes } from './types.js'; +import { getStateValues } from './utils/getStateValues.js'; export const defaultSlippage = '0.5'; @@ -36,16 +36,8 @@ export const useSettingsStore = createWithEqualityFn( set(() => ({ [key]: value, })), - setValues: (values) => - set((state) => { - const updatedState: SettingsProps = { ...state }; - for (const key in values) { - if (Object.hasOwn(state, key)) { - updatedState[key] = values[key]; - } - } - return updatedState; - }), + getSettings: () => getStateValues(get()), + getValue: (key) => get()[key], initializeTools: (toolType, tools, reset) => { if (!tools.length) { return; @@ -141,6 +133,7 @@ export const useSettingsStore = createWithEqualityFn( })); get().initializeTools('Bridges', bridges, true); get().initializeTools('Exchanges', exchanges, true); + return { ...get() }; }, }), { @@ -187,26 +180,3 @@ export const useSettingsStore = createWithEqualityFn( ) as StateCreator, Object.is, ); - -export const setDefaultSettings = (config?: WidgetConfig) => { - const { slippage, routePriority, setValue, gasPrice } = - useSettingsStore.getState(); - const defaultSlippage = - (config?.slippage || config?.sdkConfig?.routeOptions?.slippage || 0) * 100; - const defaultRoutePriority = - config?.routePriority || config?.sdkConfig?.routeOptions?.order; - defaultConfigurableSettings.slippage = ( - defaultSlippage || defaultConfigurableSettings.slippage - )?.toString(); - defaultConfigurableSettings.routePriority = - defaultRoutePriority || defaultConfigurableSettings.routePriority; - if (!slippage) { - setValue('slippage', defaultConfigurableSettings.slippage); - } - if (!routePriority) { - setValue('routePriority', defaultConfigurableSettings.routePriority); - } - if (!gasPrice) { - setValue('gasPrice', defaultConfigurableSettings.gasPrice); - } -}; diff --git a/packages/widget/src/stores/settings/utils/getStateValues.ts b/packages/widget/src/stores/settings/utils/getStateValues.ts new file mode 100644 index 000000000..d3bf241ff --- /dev/null +++ b/packages/widget/src/stores/settings/utils/getStateValues.ts @@ -0,0 +1,16 @@ +import type { SettingsProps, SettingsState } from '../types.js'; + +export const getStateValues = (state: SettingsState): SettingsProps => ({ + appearance: state.appearance, + gasPrice: state.gasPrice, + language: state.language, + routePriority: state.routePriority, + enabledAutoRefuel: state.enabledAutoRefuel, + slippage: state.slippage, + disabledBridges: [...state.disabledBridges], + enabledBridges: [...state.enabledBridges], + _enabledBridges: { ...state._enabledBridges }, + disabledExchanges: [...state.disabledExchanges], + enabledExchanges: [...state.enabledExchanges], + _enabledExchanges: { ...state._enabledExchanges }, +}); diff --git a/packages/widget/src/types/events.ts b/packages/widget/src/types/events.ts index e93fe4e2e..d59f61e6b 100644 --- a/packages/widget/src/types/events.ts +++ b/packages/widget/src/types/events.ts @@ -1,4 +1,5 @@ import type { ChainId, ChainType, Process, Route } from '@lifi/sdk'; +import type { SettingsProps } from '../stores/settings/types.js'; import type { NavigationRouteType } from '../utils/navigationRoutes.js'; export enum WidgetEvent { @@ -19,6 +20,7 @@ export enum WidgetEvent { WalletConnected = 'walletConnected', WidgetExpanded = 'widgetExpanded', PageEntered = 'pageEntered', + SettingUpdated = 'settingUpdated', } export type WidgetEvents = { @@ -36,6 +38,7 @@ export type WidgetEvents = { walletConnected: WalletConnected; widgetExpanded: boolean; pageEntered: NavigationRouteType; + settingUpdated: SettingUpdated; }; export interface ContactSupport { @@ -65,3 +68,13 @@ export interface WalletConnected { chainId?: number; chainType?: ChainType; } + +export type SettingUpdated< + K extends keyof SettingsProps = keyof SettingsProps, +> = { + setting: K; + newValue: SettingsProps[K]; + oldValue: SettingsProps[K]; + newSettings: SettingsProps; + oldSettings: SettingsProps; +}; diff --git a/packages/widget/src/utils/deepEqual.ts b/packages/widget/src/utils/deepEqual.ts new file mode 100644 index 000000000..cd80399b3 --- /dev/null +++ b/packages/widget/src/utils/deepEqual.ts @@ -0,0 +1,62 @@ +/** Forked from https://github.com/epoberezkin/fast-deep-equal */ + +export function deepEqual(a: any, b: any) { + if (a === b) { + return true; + } + + if (a && b && typeof a === "object" && typeof b === "object") { + if (a.constructor !== b.constructor) { + return false; + } + + let length: number; + let i: number; + + if (Array.isArray(a) && Array.isArray(b)) { + length = a.length; + if (length !== b.length) { + return false; + } + for (i = length; i-- !== 0; ) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + + if (a.valueOf !== Object.prototype.valueOf) { + return a.valueOf() === b.valueOf(); + } + if (a.toString !== Object.prototype.toString) { + return a.toString() === b.toString(); + } + + const keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) { + return false; + } + + for (i = length; i-- !== 0; ) { + if (!Object.prototype.hasOwnProperty.call(b, keys[i]!)) { + return false; + } + } + + for (i = length; i-- !== 0; ) { + const key = keys[i]; + + if (key && !deepEqual(a[key], b[key])) { + return false; + } + } + + return true; + } + + // true if both NaN, false otherwise + // biome-ignore lint/suspicious/noSelfCompare: + return a !== a && b !== b; +}