Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: widget events for send to wallet #309

Merged
merged 11 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
@@ -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<string, string>,
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<string, boolean>
>(initialiseStateFromWidgetEvents(WidgetEvent, allWidgetEventsOn));

useEffect(() => {
const logFunction = (eventName: string) => (value: any) =>
// eslint-disable-next-line no-console
console.info(eventName, value);

const logFunctionLookUp: Record<string, (value: any) => 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 ? (
<ExpandableCard title={'Widget Events'} value={''}>
<CardRowContainer
sx={{ paddingBottom: 1, paddingLeft: 1, paddingTop: 0 }}
>
<CapitalizeFirstLetter variant="caption">
Output for events can be viewed in the console when event listeners
are turned on
</CapitalizeFirstLetter>
</CardRowContainer>
<ControlContainer
sx={{
marginLeft: 1,
marginRight: 1,
paddingLeft: 1,
paddingRight: 1,
minHeight: 48,
}}
>
<ControlRowContainer sx={{ padding: 0 }}>
All events on page load
<Switch
checked={allWidgetEventsOn}
onChange={handleAllEventsChange}
aria-label="Toggle All Widget Events"
/>
</ControlRowContainer>
</ControlContainer>
{Object.values(WidgetEvent).map((eventName, i, arr) => (
<CardRowContainer
sx={{ paddingBottom: i < arr.length - 1 ? 0 : 2 }}
key={eventName}
>
{eventName}
<Switch
checked={monitoredEvents[eventName]}
onChange={() => handleEventChange(eventName)}
aria-label={`Enable logging of ${eventName}`}
/>
</CardRowContainer>
))}
</ExpandableCard>
) : 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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
WalletManagementControl,
} from './DesignControls';
import { FormValuesControl } from './DesignControls/FormValuesControls';
import { WidgetEventControls } from './DesignControls/WidgetEventsControls';
import {
Drawer,
DrawerContentContainer,
Expand Down Expand Up @@ -124,6 +125,7 @@ export const DrawerControls = () => {
<CardRadiusControl />
<ButtonRadiusControl />
<FormValuesControl />
<WidgetEventControls />
<WalletManagementControl />
<SkeletonControl />
<LayoutControls />
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion packages/widget-playground/src/components/Widget/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './ConnectWalletButton';
export * from './WidgetSkeleton';
export * from './WidgetView';
export * from './WidgetView.style';
export * from './WidgetViewContainer';
13 changes: 2 additions & 11 deletions packages/widget-playground/src/hooks/useDevView.ts
Original file line number Diff line number Diff line change
@@ -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);
};

Expand Down
9 changes: 9 additions & 0 deletions packages/widget-playground/src/utils/setQueryStringParam.ts
Original file line number Diff line number Diff line change
@@ -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());
};
24 changes: 24 additions & 0 deletions packages/widget/src/stores/form/FormStore.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { debounce } from '@mui/material';
import type { PropsWithChildren } from 'react';
import { useMemo, useRef } from 'react';
import type { widgetEvents } from '../../hooks/useWidgetEvents.js';
import { useWidgetEvents } from '../../hooks/useWidgetEvents.js';
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js';
import { WidgetEvent } from '../../types/events.js';
import type { FormRef, ToAddress } from '../../types/widget.js';
import { HiddenUI } from '../../types/widget.js';
import { FormStoreContext } from './FormStoreContext.js';
Expand Down Expand Up @@ -31,6 +35,17 @@ const initialiseDefaultValues = (
: toAddress?.address || formDefaultValues.toAddress,
});

const handleToAddressWidgetEvent = (
address: string,
emitter: typeof widgetEvents,
) => {
emitter.emit(WidgetEvent.SendToWalletAddressChanged, {
address,
});
};

const debouncedToAddressWidgetEvent = debounce(handleToAddressWidgetEvent, 100);

interface FormStoreProviderProps extends PropsWithChildren {
formRef?: FormRef;
}
Expand All @@ -40,6 +55,7 @@ export const FormStoreProvider: React.FC<FormStoreProviderProps> = ({
formRef,
}) => {
const widgetConfig = useWidgetConfig();
const widgetEvents = useWidgetEvents();

const {
fromChain,
Expand Down Expand Up @@ -130,6 +146,14 @@ export const FormStoreProvider: React.FC<FormStoreProviderProps> = ({
hiddenToAddress,
),
);

// emit sendToWalletAddressChange event if toAddress is set in the config
if (toAddress) {
// debounce used to push this event emit to the back of the job queue
// otherwise without it the useEffect that is set up for listening to widget events
// can potentially be set up after the event has been emitted
debouncedToAddressWidgetEvent(toAddress.address, widgetEvents);
}
DNR500 marked this conversation as resolved.
Show resolved Hide resolved
}

useFormRef(storeRef.current, formRef);
Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/stores/form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface FormProps {
touchedFields: { [key in FormFieldNames]?: boolean };
}

interface ResetOptions {
export interface ResetOptions {
defaultValue?: GenericFormValue;
}

Expand Down
82 changes: 80 additions & 2 deletions packages/widget/src/stores/form/useFieldActions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import type { FormActions } from './types.js';
import type { widgetEvents } from '../../hooks/useWidgetEvents.js';
import { useWidgetEvents } from '../../hooks/useWidgetEvents.js';
import { WidgetEvent } from '../../types/events.js';
import type {
DefaultValues,
FormActions,
FormFieldNames,
GenericFormValue,
SetOptions,
} from './types.js';
import { useFormStore } from './useFormStore.js';

interface FieldValueToEmittedEvents {
[key: string]: (
value: GenericFormValue,
emitter: typeof widgetEvents,
) => void;
}

const fieldValueToEmittedEvents: FieldValueToEmittedEvents = {
toAddress: (address, emitter) =>
Copy link
Member

@chybisov chybisov Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add all fields here to avoid adding them in the future anyway. We should also think about having one event for all of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go with one event for them all. It's simpler that way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just gonna add in better type inference too

emitter.emit(WidgetEvent.SendToWalletAddressChanged, {
address: address as string | undefined,
}),
};

const emitEventForFieldValueChange = (
fieldName: FormFieldNames,
newValue: GenericFormValue,
currenValue: GenericFormValue,
emitter: typeof widgetEvents,
) => {
const emitFunction = fieldValueToEmittedEvents[fieldName];

// only emit a widget event if a mapping exists in fieldValueToEmittedEvents
// and if the field value will change
if (emitFunction && newValue !== currenValue) {
emitFunction(newValue, emitter);
}
};

export const useFieldActions = () => {
const emitter = useWidgetEvents();
const actions = useFormStore<FormActions>(
(store) => ({
getFieldValues: store.getFieldValues,
Expand All @@ -16,5 +56,43 @@ export const useFieldActions = () => {
shallow,
);

return actions;
const setFieldValueWithEmittedEvents = useCallback(
(
fieldName: FormFieldNames,
value: GenericFormValue,
options?: SetOptions,
) => {
emitEventForFieldValueChange(
fieldName,
value,
actions.getFieldValues(fieldName)[0],
emitter,
);

actions.setFieldValue(fieldName, value, options);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we set value first and, after that fire an event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions.getFieldValues will not have the old value to do the comparison to check for change if you call actions.setFieldValue first. I tried it the other way around initially

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have changed this

},
[actions, emitter],
);

const setUserAndDefaultValuesWithEmittedEvents = useCallback(
(formValues: Partial<DefaultValues>) => {
(Object.keys(formValues) as FormFieldNames[]).forEach((fieldName) => {
emitEventForFieldValueChange(
fieldName,
formValues[fieldName],
actions.getFieldValues(fieldName)[0],
emitter,
);
});

actions.setUserAndDefaultValues(formValues);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we set value first and, after that fire an event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions.getFieldValues will not have the old value to do the comparison to check for change if you call actions.setFieldValue first. I tried it the other way around initially

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can save it to a variable like

const oldValue = actions.getFieldValues(fieldName)[0]

and then execute after.

Copy link
Contributor Author

@DNR500 DNR500 Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would actually become a bit more complicated in the example above for the setUserAndDefaultValuesWithEmittedEvents - you would have to store the oldValues in another object map first and then once setUserAndDefaultValues is executed you would have to the iterate over the old values map.

I don't see what the benefit will be of changing the order of execution there and I think it will make the code a little less cleaner - but I do it and can decide if you are happier with it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have put something in place for this

},
[actions, emitter],
);

return {
...actions,
setFieldValue: setFieldValueWithEmittedEvents,
setUserAndDefaultValues: setUserAndDefaultValuesWithEmittedEvents,
};
};
Loading