From b247afbb41b1ffd04be8f4b09862f72fc40d4a37 Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Thu, 17 Oct 2024 14:51:43 +0200 Subject: [PATCH] feat: widget events for send to wallet (#309) Co-authored-by: Eugene Chybisov <18644653+chybisov@users.noreply.github.com> --- .../DesignControls/WidgetEventsControls.tsx | 152 ++++++++++++++++++ .../DrawerControls/DrawerControls.tsx | 2 + .../src/components/Widget/WidgetSkeleton.tsx | 25 --- .../src/components/Widget/index.ts | 1 - .../widget-playground/src/hooks/useDevView.ts | 13 +- .../src/utils/setQueryStringParam.ts | 9 ++ packages/widget/src/stores/form/types.ts | 2 +- .../widget/src/stores/form/useFieldActions.ts | 75 ++++++++- packages/widget/src/types/events.ts | 11 ++ 9 files changed, 250 insertions(+), 40 deletions(-) create mode 100644 packages/widget-playground/src/components/DrawerControls/DesignControls/WidgetEventsControls.tsx delete mode 100644 packages/widget-playground/src/components/Widget/WidgetSkeleton.tsx create mode 100644 packages/widget-playground/src/utils/setQueryStringParam.ts diff --git a/packages/widget-playground/src/components/DrawerControls/DesignControls/WidgetEventsControls.tsx b/packages/widget-playground/src/components/DrawerControls/DesignControls/WidgetEventsControls.tsx new file mode 100644 index 000000000..d994e5d12 --- /dev/null +++ b/packages/widget-playground/src/components/DrawerControls/DesignControls/WidgetEventsControls.tsx @@ -0,0 +1,152 @@ +import { useWidgetEvents, WidgetEvent } from '@lifi/widget'; +import { useEffect, useState } from 'react'; +import { useDevView } from '../../../hooks'; +import { setQueryStringParam } from '../../../utils/setQueryStringParam'; +import { CardRowContainer, ExpandableCard } from '../../Card'; +import { Switch } from '../../Switch'; +import { + CapitalizeFirstLetter, + ControlContainer, + ControlRowContainer, +} from './DesignControls.style'; + +const initialiseStateFromWidgetEvents = ( + widgetEventsMap: Record, + allEventsOn: boolean = false, +) => + Object.values(widgetEventsMap).reduce((accum, eventName) => { + return { + ...accum, + [eventName]: allEventsOn, + }; + }, {}); + +export const WidgetEventControls = () => { + const { isDevView } = useDevView(); + const widgetEvents = useWidgetEvents(); + + const { allWidgetEventsOn, setAllWidgetEventsOnForPageLoad } = + useWidgetEventsSearchParam(); + const [monitoredEvents, setMonitoredEvents] = useState< + Record + >(initialiseStateFromWidgetEvents(WidgetEvent, allWidgetEventsOn)); + + useEffect(() => { + const logFunction = (eventName: string) => (value: any) => + // eslint-disable-next-line no-console + console.info(eventName, value); + + const logFunctionLookUp: Record void> = {}; + + Object.keys(monitoredEvents).forEach((eventName) => { + const eventListeningOn = monitoredEvents[eventName]; + if (eventListeningOn) { + logFunctionLookUp[eventName] = logFunction(eventName); + widgetEvents.on(eventName, logFunctionLookUp[eventName]); + } + }); + + return () => { + Object.keys(monitoredEvents).forEach((eventName) => { + const eventListeningOn = monitoredEvents[eventName]; + if (eventListeningOn) { + widgetEvents.off(eventName, logFunctionLookUp[eventName]); + delete logFunctionLookUp[eventName]; + } + }); + }; + }, [widgetEvents, monitoredEvents]); + + const handleAllEventsChange = () => { + const areAllEventsOn = !allWidgetEventsOn; + + setAllWidgetEventsOnForPageLoad(areAllEventsOn); + + setMonitoredEvents( + initialiseStateFromWidgetEvents(WidgetEvent, areAllEventsOn), + ); + }; + + const handleEventChange = (eventName: string) => { + const newEventsMap = { + ...monitoredEvents, + [eventName]: !monitoredEvents[eventName], + }; + + setMonitoredEvents(newEventsMap); + + const areAllEventsOn = Object.values(newEventsMap).every( + (eventOn) => eventOn, + ); + setAllWidgetEventsOnForPageLoad(areAllEventsOn); + }; + + return isDevView ? ( + + + + Output for events can be viewed in the console when event listeners + are turned on + + + + + All events on page load + + + + {Object.values(WidgetEvent).map((eventName, i, arr) => ( + + {eventName} + handleEventChange(eventName)} + aria-label={`Enable logging of ${eventName}`} + /> + + ))} + + ) : null; +}; + +const getAllWidgetEventsOnFromQueryString = () => { + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + return !!urlParams.get('allWidgetEvents') || false; + } + return false; +}; + +const useWidgetEventsSearchParam = () => { + const [allWidgetEventsOn, setAllWidgetEventsOn] = useState( + getAllWidgetEventsOnFromQueryString(), + ); + + const setAllWidgetEventsOnForPageLoad = (on: boolean) => { + setQueryStringParam('allWidgetEvents', on); + + setAllWidgetEventsOn(on); + }; + + return { + allWidgetEventsOn, + setAllWidgetEventsOnForPageLoad, + }; +}; diff --git a/packages/widget-playground/src/components/DrawerControls/DrawerControls.tsx b/packages/widget-playground/src/components/DrawerControls/DrawerControls.tsx index d5cdff104..44a4e2820 100644 --- a/packages/widget-playground/src/components/DrawerControls/DrawerControls.tsx +++ b/packages/widget-playground/src/components/DrawerControls/DrawerControls.tsx @@ -28,6 +28,7 @@ import { WalletManagementControl, } from './DesignControls'; import { FormValuesControl } from './DesignControls/FormValuesControls'; +import { WidgetEventControls } from './DesignControls/WidgetEventsControls'; import { Drawer, DrawerContentContainer, @@ -124,6 +125,7 @@ export const DrawerControls = () => { + diff --git a/packages/widget-playground/src/components/Widget/WidgetSkeleton.tsx b/packages/widget-playground/src/components/Widget/WidgetSkeleton.tsx deleted file mode 100644 index d39d5f8e4..000000000 --- a/packages/widget-playground/src/components/Widget/WidgetSkeleton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box } from '@mui/material'; -import { Skeleton, WidgetSkeletonContainer } from './WidgetView.style'; - -export const WidgetSkeleton = () => { - return ( - - - - - - - - - - - - - ); -}; diff --git a/packages/widget-playground/src/components/Widget/index.ts b/packages/widget-playground/src/components/Widget/index.ts index cc4ad418f..a8623726e 100644 --- a/packages/widget-playground/src/components/Widget/index.ts +++ b/packages/widget-playground/src/components/Widget/index.ts @@ -1,5 +1,4 @@ export * from './ConnectWalletButton'; -export * from './WidgetSkeleton'; export * from './WidgetView'; export * from './WidgetView.style'; export * from './WidgetViewContainer'; diff --git a/packages/widget-playground/src/hooks/useDevView.ts b/packages/widget-playground/src/hooks/useDevView.ts index d669aee0a..2e42b63d0 100644 --- a/packages/widget-playground/src/hooks/useDevView.ts +++ b/packages/widget-playground/src/hooks/useDevView.ts @@ -1,25 +1,16 @@ import { shallow } from 'zustand/shallow'; import { useEditToolsActions, useEditToolsStore } from '../store'; +import { setQueryStringParam } from '../utils/setQueryStringParam'; const queryStringKey = 'devView'; -const setQueryStringParam = (value: boolean) => { - const url = new URL(window.location.href); - if (value) { - url.searchParams.set(queryStringKey, value.toString()); - } else { - url.searchParams.delete(queryStringKey); - } - window.history.pushState(null, '', url.toString()); -}; - export const useDevView = () => { const [isDevView] = useEditToolsStore((store) => [store.isDevView], shallow); const { setIsDevView } = useEditToolsActions(); const toggleDevView = () => { const newDevViewValue = !isDevView; - setQueryStringParam(newDevViewValue); + setQueryStringParam(queryStringKey, newDevViewValue); setIsDevView(newDevViewValue); }; diff --git a/packages/widget-playground/src/utils/setQueryStringParam.ts b/packages/widget-playground/src/utils/setQueryStringParam.ts new file mode 100644 index 000000000..aeff75263 --- /dev/null +++ b/packages/widget-playground/src/utils/setQueryStringParam.ts @@ -0,0 +1,9 @@ +export const setQueryStringParam = (queryStringKey: string, value: boolean) => { + const url = new URL(window.location.href); + if (value) { + url.searchParams.set(queryStringKey, value.toString()); + } else { + url.searchParams.delete(queryStringKey); + } + window.history.pushState(null, '', url.toString()); +}; diff --git a/packages/widget/src/stores/form/types.ts b/packages/widget/src/stores/form/types.ts index f8fae8d51..1caff4929 100644 --- a/packages/widget/src/stores/form/types.ts +++ b/packages/widget/src/stores/form/types.ts @@ -64,7 +64,7 @@ export interface FormProps { touchedFields: { [key in FormFieldNames]?: boolean }; } -interface ResetOptions { +export interface ResetOptions { defaultValue?: GenericFormValue; } diff --git a/packages/widget/src/stores/form/useFieldActions.ts b/packages/widget/src/stores/form/useFieldActions.ts index 87c2e2ca3..64cd2d1bd 100644 --- a/packages/widget/src/stores/form/useFieldActions.ts +++ b/packages/widget/src/stores/form/useFieldActions.ts @@ -1,8 +1,19 @@ +import { useCallback } from 'react'; import { shallow } from 'zustand/shallow'; -import type { FormActions } from './types.js'; +import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'; +import type { FormFieldChanged } from '../../types/events.js'; +import { WidgetEvent } from '../../types/events.js'; +import type { + DefaultValues, + FormActions, + FormFieldNames, + GenericFormValue, + SetOptions, +} from './types.js'; import { useFormStore } from './useFormStore.js'; export const useFieldActions = () => { + const emitter = useWidgetEvents(); const actions = useFormStore( (store) => ({ getFieldValues: store.getFieldValues, @@ -16,5 +27,65 @@ export const useFieldActions = () => { shallow, ); - return actions; + const setFieldValueWithEmittedEvents = useCallback( + ( + fieldName: FormFieldNames, + newValue: GenericFormValue, + options?: SetOptions, + ) => { + const oldValue = actions.getFieldValues(fieldName)[0]; + + actions.setFieldValue(fieldName, newValue, options); + + if (newValue !== oldValue) { + emitter.emit(WidgetEvent.FormFieldChanged, { + fieldName, + newValue, + oldValue, + } as FormFieldChanged); + } + }, + [actions, emitter], + ); + + const setUserAndDefaultValuesWithEmittedEvents = useCallback( + (formValues: Partial) => { + const formValuesKeys = Object.keys(formValues) as FormFieldNames[]; + + const changedValues = formValuesKeys.reduce( + (accum, fieldName) => { + const oldValue = actions.getFieldValues(fieldName)[0]; + const newValue = formValues[fieldName]; + + if (newValue !== oldValue) { + accum.push({ fieldName, newValue, oldValue }); + } + + return accum; + }, + [] as { + fieldName: FormFieldNames; + newValue: GenericFormValue; + oldValue: GenericFormValue; + }[], + ); + + actions.setUserAndDefaultValues(formValues); + + changedValues.forEach(({ fieldName, newValue, oldValue }) => { + emitter.emit(WidgetEvent.FormFieldChanged, { + fieldName, + newValue, + oldValue, + } as FormFieldChanged); + }); + }, + [actions, emitter], + ); + + return { + ...actions, + setFieldValue: setFieldValueWithEmittedEvents, + setUserAndDefaultValues: setUserAndDefaultValuesWithEmittedEvents, + }; }; diff --git a/packages/widget/src/types/events.ts b/packages/widget/src/types/events.ts index d59f61e6b..9e192a1ec 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 { DefaultValues } from '@lifi/widget/stores/form/types.js'; import type { SettingsProps } from '../stores/settings/types.js'; import type { NavigationRouteType } from '../utils/navigationRoutes.js'; @@ -20,6 +21,7 @@ export enum WidgetEvent { WalletConnected = 'walletConnected', WidgetExpanded = 'widgetExpanded', PageEntered = 'pageEntered', + FormFieldChanged = 'formFieldChanged', SettingUpdated = 'settingUpdated', } @@ -34,6 +36,7 @@ export type WidgetEvents = { sourceChainTokenSelected: ChainTokenSelected; destinationChainTokenSelected: ChainTokenSelected; sendToWalletToggled: boolean; + formFieldChanged: FormFieldChanged; reviewTransactionPageEntered?: Route; walletConnected: WalletConnected; widgetExpanded: boolean; @@ -69,6 +72,14 @@ export interface WalletConnected { chainType?: ChainType; } +export type FormFieldChanged = { + [K in keyof DefaultValues]: { + fieldName: K; + newValue: DefaultValues[K]; + oldValue: DefaultValues[K]; + }; +}[keyof DefaultValues]; + export type SettingUpdated< K extends keyof SettingsProps = keyof SettingsProps, > = {