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());
};
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
75 changes: 73 additions & 2 deletions packages/widget/src/stores/form/useFieldActions.ts
Original file line number Diff line number Diff line change
@@ -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<FormActions>(
(store) => ({
getFieldValues: store.getFieldValues,
Expand All @@ -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<DefaultValues>) => {
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,
};
};
11 changes: 11 additions & 0 deletions packages/widget/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -20,6 +21,7 @@ export enum WidgetEvent {
WalletConnected = 'walletConnected',
WidgetExpanded = 'widgetExpanded',
PageEntered = 'pageEntered',
FormFieldChanged = 'formFieldChanged',
SettingUpdated = 'settingUpdated',
}

Expand All @@ -34,6 +36,7 @@ export type WidgetEvents = {
sourceChainTokenSelected: ChainTokenSelected;
destinationChainTokenSelected: ChainTokenSelected;
sendToWalletToggled: boolean;
formFieldChanged: FormFieldChanged;
reviewTransactionPageEntered?: Route;
walletConnected: WalletConnected;
widgetExpanded: boolean;
Expand Down Expand Up @@ -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,
> = {
Expand Down