diff --git a/src/blocks/icons/components/Pencil.tsx b/src/blocks/icons/components/Pencil.tsx new file mode 100644 index 0000000000..a7b5235380 --- /dev/null +++ b/src/blocks/icons/components/Pencil.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; +import { IconWrapper } from '../IconWrapper'; +import { IconProps } from '../Icons.types'; + +const Pencil: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + } + {...restProps} + /> + ); +}; + +export default Pencil; diff --git a/src/blocks/icons/index.ts b/src/blocks/icons/index.ts index dfc0a4bb33..eaa4e47ac8 100644 --- a/src/blocks/icons/index.ts +++ b/src/blocks/icons/index.ts @@ -98,6 +98,8 @@ export { default as NotificationMobile } from './components/NotificationMobile'; export { default as OptOut } from './components/OptOut'; +export { default as Pencil } from './components/Pencil'; + export { default as PlusCircle } from './components/PlusCircle'; export { default as PlusCircleFilled } from './components/PlusCircleFilled'; diff --git a/src/blocks/modal/Modal.tsx b/src/blocks/modal/Modal.tsx index 339ea89858..b83d5b3d83 100644 --- a/src/blocks/modal/Modal.tsx +++ b/src/blocks/modal/Modal.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { Button, ButtonProps } from '../button'; import { Back, Cross } from '../icons'; import { ModalSize } from './Modal.types'; +import { deviceMediaQ } from 'blocks/theme'; type ButtonAlignment = 'end' | 'center'; @@ -27,7 +28,7 @@ const Overlay = styled(Dialog.Overlay)` z-index: 1000; `; -const ContentContainer = styled(Dialog.Content)<{ size: ModalSize }>` +const ContentContainer = styled(Dialog.Content) <{ size: ModalSize }>` display: flex; border-radius: var(--radius-sm); border: var(--border-sm) solid var(--stroke-secondary); @@ -43,12 +44,15 @@ const ContentContainer = styled(Dialog.Content)<{ size: ModalSize }>` width: ${({ size }) => (size === 'small' ? '360px' : size === 'medium' ? '500px' : '700px')}; gap: var(--spacing-sm); z-index: 1100; + @media ${deviceMediaQ.mobileL}{ + width:80%; + } `; const ContentChildren = styled.div<{ size: ModalSize }>` display: flex; flex-direction: column; - align-items: flex-start; + align-items: flex-start; flex: 1 0 0; width: 100%; padding-top: var(--spacing-${({ size }) => (size === 'small' ? 'xxs' : 'xs')}); diff --git a/src/blocks/textInput/TextInput.tsx b/src/blocks/textInput/TextInput.tsx index 5d2502a1f5..5ea294f785 100644 --- a/src/blocks/textInput/TextInput.tsx +++ b/src/blocks/textInput/TextInput.tsx @@ -9,7 +9,7 @@ export type TextInputProps = { disabled?: boolean; icon?: ReactNode; error?: boolean; - type?: 'text' | 'password'; + type?: 'text' | 'password' | 'number'; errorMessage?: string; label?: string; onChange?: (e: React.ChangeEvent) => void; @@ -18,7 +18,7 @@ export type TextInputProps = { required?: boolean; success?: boolean; totalCount?: number; - value: string; + value: string | number; }; const Container = styled.div<{ css?: FlattenSimpleInterpolation }>` @@ -168,7 +168,7 @@ export const TextInput = forwardRef( color={disabled ? 'components-inputs-text-disabled' : 'components-inputs-text-secondary'} variant="c-regular" > - {`${value?.length || 0} / ${totalCount}`} + {`${(typeof value === 'string' && value?.length) || 0} / ${totalCount}`} )} @@ -199,8 +199,8 @@ export const TextInput = forwardRef( success || error ? 'components-inputs-text-default' : disabled - ? 'components-inputs-text-disabled' - : 'components-inputs-text-placeholder' + ? 'components-inputs-text-disabled' + : 'components-inputs-text-placeholder' } variant="c-regular" > diff --git a/src/common/Common.form.ts b/src/common/Common.form.ts index 91993080a4..a570b6652e 100644 --- a/src/common/Common.form.ts +++ b/src/common/Common.form.ts @@ -3,3 +3,7 @@ export const URLRegex = /^(http:\/\/|https:\/\/|www\.)?([\w-]+\.)+[\w-]{2,}(\/[\ export const getRequiredFieldMessage = (name: string) => `${name} is required.`; export const getMaxCharLimitFieldMessage = (limit: number) => `Maximum ${limit} characters allowed.`; + +export const getMinNumValueMessage = (limit: number | string) => `Must be greater than ${limit}`; + +export const getRangeValueMessage = (name: string) => `${name} must be within the defined range limits.`; diff --git a/src/common/Common.types.ts b/src/common/Common.types.ts new file mode 100644 index 0000000000..931d784879 --- /dev/null +++ b/src/common/Common.types.ts @@ -0,0 +1,5 @@ +export type ModalResponse = { + isOpen: boolean; + onClose: () => void; + open: () => void; +}; diff --git a/src/common/hooks/index.ts b/src/common/hooks/index.ts index 871bdf7d48..d872d65587 100644 --- a/src/common/hooks/index.ts +++ b/src/common/hooks/index.ts @@ -1,2 +1,3 @@ export { useIsVisible } from './useIsVisible'; export { usePushStakingStats } from './usePushStakingStats'; +export { useDisclosure } from './useDisclosure'; diff --git a/src/common/hooks/useDisclosure.ts b/src/common/hooks/useDisclosure.ts new file mode 100644 index 0000000000..8c328c1fc9 --- /dev/null +++ b/src/common/hooks/useDisclosure.ts @@ -0,0 +1,17 @@ +import { ModalResponse } from 'common/Common.types'; +import { useState, useCallback } from 'react'; + +const useDisclosure = (): ModalResponse => { + const [isOpen, setIsOpen] = useState(false); + + const open = useCallback(() => setIsOpen(true), []); + const onClose = useCallback(() => setIsOpen(false), []); + + return { + isOpen, + onClose, + open, + }; +}; + +export { useDisclosure }; diff --git a/src/common/index.ts b/src/common/index.ts index 43949e8236..7cf9ee02f2 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -3,3 +3,4 @@ export * from './components'; export * from './Common.constants'; export * from './Common.utils'; export * from './Common.form'; +export * from './Common.types'; diff --git a/src/components/reusables/checkbox/Checkbox.tsx b/src/components/reusables/checkbox/Checkbox.tsx index d896c536d7..a298b60673 100644 --- a/src/components/reusables/checkbox/Checkbox.tsx +++ b/src/components/reusables/checkbox/Checkbox.tsx @@ -25,7 +25,7 @@ const CheckboxInput = styled.input` /* Change the color of the checkbox */ &:checked { - accent-color: #D53A94; + accent-color: #C742DD; } `; diff --git a/src/components/reusables/sliders/InputSlider.tsx b/src/components/reusables/sliders/InputSlider.tsx index 74f7064a39..f23b247b37 100644 --- a/src/components/reusables/sliders/InputSlider.tsx +++ b/src/components/reusables/sliders/InputSlider.tsx @@ -153,7 +153,7 @@ const Container = styled.div` const PreviewContainer = styled.div` display: none; position: absolute; - bottom: -48px; + top: -48px; border-radius: 4px; border: 1px solid ${(props) => props.theme.default.border}; background: ${(props) => props.theme.default.bg}; diff --git a/src/components/reusables/sliders/RangeSlider.tsx b/src/components/reusables/sliders/RangeSlider.tsx index 78e41becc1..cd9e2348db 100644 --- a/src/components/reusables/sliders/RangeSlider.tsx +++ b/src/components/reusables/sliders/RangeSlider.tsx @@ -243,7 +243,7 @@ const Container = styled.div` const PreviewContainer = styled.div` display: none; position: absolute; - bottom: -48px; + top: -48px; border-radius: 4px; border: 1px solid ${(props) => props.theme.default.border}; background: ${(props) => props.theme.default.bg}; diff --git a/src/modules/channelDashboard/ChannelDashboard.types.ts b/src/modules/channelDashboard/ChannelDashboard.types.ts index 67efdee8f4..38cbf5de9d 100644 --- a/src/modules/channelDashboard/ChannelDashboard.types.ts +++ b/src/modules/channelDashboard/ChannelDashboard.types.ts @@ -1,5 +1,5 @@ export type ChannelSetting = { - type: 1 | 2 | 3; + type: number; default: | boolean | number @@ -7,12 +7,12 @@ export type ChannelSetting = { lower: number; upper: number; }; - enabled: boolean; + enabled?: boolean; description: string; - index: number; - lowerLimit: number; - upperLimit: number; - ticker: number; + index?: number; + lowerLimit?: number; + upperLimit?: number; + ticker?: number; }; export type DashboardActiveState = diff --git a/src/modules/channelDashboard/components/ChannelDashboardNullState.tsx b/src/modules/channelDashboard/components/ChannelDashboardNullState.tsx index 75072a9193..e059c43667 100644 --- a/src/modules/channelDashboard/components/ChannelDashboardNullState.tsx +++ b/src/modules/channelDashboard/components/ChannelDashboardNullState.tsx @@ -1,6 +1,6 @@ -import { Box, Button, CrownSimple, ReceiveNotification, Text } from "blocks"; -import { FC } from "react"; +import { FC } from 'react'; +import { Box, Button, CrownSimple, ReceiveNotification, Text } from 'blocks'; type ChannelDashboardNullStateProps = { state: 'notificationSettings' | 'delegatee'; @@ -9,12 +9,7 @@ type ChannelDashboardNullStateProps = { onClick?: any; }; -const ChannelDashboardNullState: FC = ({ - state, - title, - subTitle, - onClick -}) => { +const ChannelDashboardNullState: FC = ({ state, title, subTitle, onClick }) => { return ( = ({ gap="spacing-sm" height="200px" > - {state == 'delegatee' && } - {state == 'notificationSettings' && } + {state == 'delegatee' && ( + + )} + {state == 'notificationSettings' && ( + + )} - - + + {title} - + {subTitle} {onClick && ( - )} @@ -46,4 +67,4 @@ const ChannelDashboardNullState: FC = ({ ); }; -export { ChannelDashboardNullState }; \ No newline at end of file +export { ChannelDashboardNullState }; diff --git a/src/modules/notifSettings/EditNotificationSetting.form.tsx b/src/modules/notifSettings/EditNotificationSetting.form.tsx new file mode 100644 index 0000000000..d56aaac97b --- /dev/null +++ b/src/modules/notifSettings/EditNotificationSetting.form.tsx @@ -0,0 +1,142 @@ +import { FC } from 'react'; + +import * as Yup from 'yup'; + +import { FormikProvider, useFormik, useFormikContext } from 'formik'; + +import { getMinNumValueMessage, getRangeValueMessage, getRequiredFieldMessage } from 'common'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +type EditNotificationSettingsFormProviderProps = { + children: React.ReactNode; + initialValue: ChannelSetting; + onSubmit: (values: NotificationSettingFormValues) => void; +}; + +export const getFormInitialValus = (initialValue: ChannelSetting): NotificationSettingFormValues => { + return { + settingName: initialValue.description, + isDefault: + initialValue.type === 1 + ? typeof initialValue.default === 'boolean' + ? initialValue.default + : true + : initialValue.enabled!, + enableRange: initialValue.type !== 1 ? true : false, + rangelowerlimit: initialValue.lowerLimit ? initialValue.lowerLimit : 0, + rangeupperlimit: initialValue.upperLimit ? initialValue.upperLimit : 0, + enableMultiRange: initialValue.type === 3 ? true : false, + defaultValue: typeof initialValue.default === 'number' ? initialValue.default : 0, + multirangelowerlimit: typeof initialValue.default === 'object' ? initialValue.default.lower : 0, + multirangeupperlimit: typeof initialValue.default === 'object' ? initialValue.default.upper : 0, + sliderStepValue: initialValue.ticker ? initialValue.ticker : 0, + }; +}; + +export const NotificationSettingsSchema = Yup.object().shape({ + settingName: Yup.string().required(getRequiredFieldMessage('Setting Name')), + isDefault: Yup.boolean(), + enableRange: Yup.boolean(), + + rangelowerlimit: Yup.number().when('enableRange', { + is: true, + then: () => Yup.number().min(1, getMinNumValueMessage(1)).required(getRequiredFieldMessage('Range')), + otherwise: () => Yup.number(), + }), + + rangeupperlimit: Yup.number().when('enableRange', { + is: true, + then: () => + Yup.number() + .min(Yup.ref('rangelowerlimit'), getMinNumValueMessage('Lower limit')) + .required(getRequiredFieldMessage('Range')), + otherwise: () => Yup.number(), + }), + + enableMultiRange: Yup.boolean().required(getRequiredFieldMessage('')), + + multirangelowerlimit: Yup.number().when(['enableMultiRange', 'enableRange'], { + is: (enableMultiRange: boolean, enableRange: boolean) => enableMultiRange && enableRange, + then: () => + Yup.number() + .min(1, getMinNumValueMessage(1)) + .required(getRequiredFieldMessage('Range')) + .test('is-multi-range-within-range', getRangeValueMessage('Multi-range lower limit'), (value, context) => { + const { rangelowerlimit, rangeupperlimit } = context.parent; + return value >= rangelowerlimit && value < rangeupperlimit; + }), + otherwise: () => Yup.number(), + }), + + multirangeupperlimit: Yup.number().when(['enableMultiRange', 'enableRange'], { + is: (enableMultiRange: boolean, enableRange: boolean) => enableMultiRange && enableRange, + then: () => + Yup.number() + .min(Yup.ref('multirangelowerlimit'), getMinNumValueMessage('Lower limit')) + .required(getRequiredFieldMessage('Range')) + .test( + 'is-multi-range-upper-within-range', + getRangeValueMessage('Multi-range upper limit'), + (value, context) => { + const { rangelowerlimit, rangeupperlimit } = context.parent; + return value > rangelowerlimit && value <= rangeupperlimit; + } + ), + otherwise: () => Yup.number(), + }), + + defaultValue: Yup.number().when(['enableMultiRange', 'enableRange'], { + is: (enableMultiRange: boolean, enableRange: boolean) => !enableMultiRange && enableRange, + then: () => + Yup.number() + .min(0, getMinNumValueMessage(0)) + .required(getRequiredFieldMessage('Default Value')) + .test('is-within-range', getRangeValueMessage('Default value'), (value, context) => { + const { rangelowerlimit, rangeupperlimit } = context.parent; + return value >= rangelowerlimit && value <= rangeupperlimit; + }), + otherwise: () => Yup.number(), + }), + + sliderStepValue: Yup.number().when('enableRange', { + is: true, + then: () => + Yup.number() + .min(1, getMinNumValueMessage(1)) + .required(getRequiredFieldMessage('Slider Step')) + .test('is-step-value-valid', 'Slider step value must not exceed the range limits.', (value, context) => { + const { rangeupperlimit } = context.parent; + return value < rangeupperlimit; + }), + otherwise: () => Yup.number(), + }), +}); + +const EditNotificationSettingsFormProvider: FC = ({ + children, + initialValue, + onSubmit, +}) => { + const initialValues = getFormInitialValus(initialValue); + + const addNotificationSettingsForm = useFormik({ + initialValues, + enableReinitialize: true, + validationSchema: NotificationSettingsSchema, + onSubmit: onSubmit, + }); + + return {children}; +}; + +const useEditNotificationSettingsForm = () => { + const context = useFormikContext(); + + if (!context) { + throw new Error('useEditNotificationSettingsForm must be used within a EditNotificationSettingsFormProvider'); + } + return context; +}; + +export { useEditNotificationSettingsForm, EditNotificationSettingsFormProvider }; diff --git a/src/modules/notifSettings/NotificationSettings.constants.ts b/src/modules/notifSettings/NotificationSettings.constants.ts new file mode 100644 index 0000000000..b6e89f7db5 --- /dev/null +++ b/src/modules/notifSettings/NotificationSettings.constants.ts @@ -0,0 +1,18 @@ +export const settingInitialValue = { + type: 1, + default: 0, + description: '', + index: 0, +}; + +export function compareObject(obj1: any, obj2: any): boolean { + if (obj1 === obj2) return true; + if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) return false; + + return keys1.every((key) => compareObject(obj1[key], obj2[key])); +} diff --git a/src/modules/notifSettings/NotificationSettings.tsx b/src/modules/notifSettings/NotificationSettings.tsx new file mode 100644 index 0000000000..c9a3bb76bd --- /dev/null +++ b/src/modules/notifSettings/NotificationSettings.tsx @@ -0,0 +1,56 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { Box } from 'blocks'; + +import { useDisclosure } from 'common'; + +import APP_PATHS from 'config/AppPaths'; + +import { useAccount } from 'hooks'; + +import { useGetChannelDetails } from 'queries'; + +import { NotificationSettingsComponent } from './components/NotificationSettingsComponent'; + +const NotificationSettings = () => { + const { account } = useAccount(); + const modalControl = useDisclosure(); + const navigate = useNavigate(); + + const { data: channelDetails, isLoading: loadingChannelDetails } = useGetChannelDetails(account); + const channelSettings = channelDetails?.channel_settings ?? ''; + + const modifiedChannelSettings = loadingChannelDetails + ? Array(3).fill(0) + : channelSettings + ? JSON.parse(channelSettings) + : []; + + useEffect(() => { + if (!channelDetails && !loadingChannelDetails) { + navigate(`${APP_PATHS.Channels}`); + } + }, [channelDetails]); + + return ( + + + + ); +}; + +export { NotificationSettings }; diff --git a/src/modules/notifSettings/NotificationSettings.types.ts b/src/modules/notifSettings/NotificationSettings.types.ts new file mode 100644 index 0000000000..2390c4651c --- /dev/null +++ b/src/modules/notifSettings/NotificationSettings.types.ts @@ -0,0 +1,35 @@ +type NotificationSettingFormValues = { + settingName: string; + isDefault: boolean; + enableRange: boolean; + rangelowerlimit: number; + rangeupperlimit: number; + enableMultiRange: boolean; + defaultValue: number; + multirangelowerlimit: number; + multirangeupperlimit: number; + sliderStepValue: number; +}; + +type NotificationSettings = { + type: number; + default: + | boolean + | number + | { + lower: number; + upper: number; + }; + enabled?: boolean; + description: string; + index?: number; + lowerLimit?: number; + upperLimit?: number; + ticker?: number; +}; + +type NotificationSettingModal = { + isOpen: boolean; + onClose: () => void; + open: () => void; +}; diff --git a/src/modules/notifSettings/components/AddNotificationSettingsModalContent.tsx b/src/modules/notifSettings/components/AddNotificationSettingsModalContent.tsx new file mode 100644 index 0000000000..26cd9438b8 --- /dev/null +++ b/src/modules/notifSettings/components/AddNotificationSettingsModalContent.tsx @@ -0,0 +1,104 @@ +import { FC } from 'react'; + +import { Box, Text, TextInput, ToggleSwitch } from 'blocks'; +import { css } from 'styled-components'; + +import { NotificationSettingsRangeSelector } from './NotificationSettingsRangeSelector'; +import { useEditNotificationSettingsForm } from '../EditNotificationSetting.form'; + +type AddNotificationSettingsModalContentProps = { + onClose: () => void; +}; + +const AddNotificationSettingsModalContent: FC = () => { + const { values: formValues, handleChange, touched, errors, setFieldValue } = useEditNotificationSettingsForm(); + + return ( + +
{}}> + + + Add a Setting + + + + + + + + Set as Default + + Setting on for users by default + + setFieldValue('isDefault', checked)} + /> + + + + + + Range + + Set a range for this setting e.g. 1-10 + + setFieldValue('enableRange', checked)} + /> + + {formValues.enableRange && } + + +
+
+ ); +}; + +export { AddNotificationSettingsModalContent }; diff --git a/src/modules/notifSettings/components/NotificationSettingsComponent.tsx b/src/modules/notifSettings/components/NotificationSettingsComponent.tsx new file mode 100644 index 0000000000..4a895a4b8c --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsComponent.tsx @@ -0,0 +1,96 @@ +import { FC, useEffect, useState } from 'react'; + +import { ModalResponse } from 'common'; + +import { useAccount } from 'hooks'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; +import { ChannelDashboardNullState } from 'modules/channelDashboard/components/ChannelDashboardNullState'; + +import { NotificationSettingsHeader } from './NotificationSettingsHeader'; +import { settingInitialValue } from '../NotificationSettings.constants'; +import { NotificationSettingsLists } from './NotificationSettingsLists'; +import { NotificationSettingsFooter } from './NotificationSettingsFooter'; +import { NotificationSettingsModal } from './NotificationSettingsModal'; + +type NotificationSettingsComponentProps = { + modalControl: ModalResponse; + channelSettings: ChannelSetting[]; + loadingSettings: boolean; +}; + +const NotificationSettingsComponent: FC = ({ + modalControl, + channelSettings, + loadingSettings, +}) => { + const { open } = modalControl; + const { isWalletConnected, connect } = useAccount(); + const [settingsToEdit, setSettingsToEdit] = useState(settingInitialValue); + + const [newSettings, setNewSettings] = useState([]); + + useEffect(() => { + if (channelSettings && !loadingSettings) { + setNewSettings(channelSettings); + } + }, [loadingSettings]); + + const handleDeleteSetting = (settingToDelete: ChannelSetting) => { + setNewSettings((settings) => settings.filter((setting) => setting.index !== settingToDelete.index)); + }; + + const handleSettingsChange = (newSetting: ChannelSetting) => { + const index = newSettings.findIndex((setting) => setting.index === newSetting.index); + if (index === -1) setNewSettings([...newSettings, newSetting]); + else { + const updatedSetting = newSettings.map((setting, settingIndex) => + settingIndex === index ? { ...newSetting } : setting + ); + setNewSettings(updatedSetting); + } + }; + + const openModal = () => (!isWalletConnected ? connect() : open()); + + return ( + <> + + + {newSettings.length > 0 ? ( + + ) : ( + + )} + + + + + + ); +}; + +export { NotificationSettingsComponent }; diff --git a/src/modules/notifSettings/components/NotificationSettingsFooter.tsx b/src/modules/notifSettings/components/NotificationSettingsFooter.tsx new file mode 100644 index 0000000000..fd1d8a90f6 --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsFooter.tsx @@ -0,0 +1,268 @@ +import { FC, useEffect, useMemo, useState } from 'react'; + +import { ethers } from 'ethers'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { Alert, Box, Button } from 'blocks'; + +import { StakingVariant } from 'common'; +import { addresses } from 'config'; +import APP_PATHS from 'config/AppPaths'; +import { useAppContext } from 'contexts/AppContext'; + +import { getPushTokenApprovalAmount, getPushTokenFromWallet } from 'helpers'; +import { useAccount } from 'hooks'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; +import { useApprovePUSHToken, useCreateNotificationSettings } from 'queries'; +import useFetchChannelDetails from 'common/hooks/useFetchUsersChannelDetails'; +import { compareObject } from '../NotificationSettings.constants'; + +type NotificationSettingsFooterProps = { + newSettings: ChannelSetting[]; + channelSettings: ChannelSetting[]; +}; + +const EDIT_SETTING_FEE = 50; + +const NotificationSettingsFooter: FC = ({ newSettings, channelSettings }) => { + const navigate = useNavigate(); + const { account, provider, wallet, isWalletConnected, connect } = useAccount(); + + const { userPushSDKInstance } = useSelector((state: any) => { + return state.user; + }); + + const { handleConnectWalletAndEnableProfile } = useAppContext(); + const { refetchChannelDetails } = useFetchChannelDetails(); + + const [pushApprovalAmount, setPushApprovalAmount] = useState(0); + const [errorMessage, setErrorMessage] = useState(''); + const [balance, setBalance] = useState(0); + + // Check PUSH Token in wallet + const pushTokenInWallet = async () => { + const amount = await getPushTokenFromWallet({ + address: account, + provider: provider, + }); + setBalance(amount); + }; + + const checkApprovedPUSHTokenAmount = async () => { + const pushTokenApprovalAmount = await getPushTokenApprovalAmount({ + address: account, + provider: provider, + contractAddress: addresses.epnscore, + }); + setPushApprovalAmount(parseInt(pushTokenApprovalAmount)); + }; + + useEffect(() => { + if (!account || !provider) return; + checkApprovedPUSHTokenAmount(); + pushTokenInWallet(); + }, [account, provider]); + + const { mutate: approvePUSHToken, isPending: approvingPUSH } = useApprovePUSHToken(); + + const approvePUSH = async () => { + if (!provider) return; + + if (!isWalletConnected) { + connect(); + return; + } + + setErrorMessage(''); + const signer = provider.getSigner(account); + const fees = ethers.utils.parseUnits((EDIT_SETTING_FEE - pushApprovalAmount).toString(), 18); + + approvePUSHToken( + { + noOfTokenToApprove: fees, + signer, + }, + { + onSuccess: () => { + checkApprovedPUSHTokenAmount(); + }, + onError: (error: any) => { + console.log('Error in Approving PUSH', error); + if (error.code == 'ACTION_REJECTED') { + setErrorMessage('User rejected signature. Please try again.'); + } else { + setErrorMessage('Error in approving PUSH Tokens'); + } + }, + } + ); + }; + + const { mutate: createNotificationSettings, isPending: addingNotificationSettings } = useCreateNotificationSettings(); + + const handleSaveSettings = async () => { + let userPushInstance = userPushSDKInstance; + if (!userPushInstance.signer) { + userPushInstance = await handleConnectWalletAndEnableProfile({ wallet }); + if (!userPushInstance) { + return; + } + } + + setErrorMessage(''); + const newsettingData: ChannelSetting[] = newSettings.map((setting) => { + if (setting.type === 1) { + return { + type: setting.type, + description: setting.description, + default: setting.default ? 1 : 0, + }; + } else { + return { + type: setting.type, + description: setting.description, + default: setting.default, + data: { + lower: setting.lowerLimit, + upper: setting.upperLimit, + ticker: setting.ticker, + enabled: setting.enabled, + }, + }; + } + }); + + createNotificationSettings( + { + userPushSDKInstance: userPushInstance, + settings: newsettingData, + }, + { + onSuccess: (response) => { + if (response.transactionHash) { + refetchChannelDetails(); + navigate(`${APP_PATHS.ChannelDashboard}/${account}`); + } + }, + onError: (error: any) => { + console.log('Error in adding setting', error); + setErrorMessage('Error in saving settings. Please try again later'); + }, + } + ); + }; + + const settingsChanged = useMemo(() => { + if (!channelSettings) return false; + if (newSettings.length !== channelSettings.length) return true; + + let isChanged = false; + + newSettings.forEach((setting1, index) => { + const setting2 = channelSettings[index]; + + if (setting1.type !== setting2.type) { + isChanged = true; + return; + } + + if (setting1.type === 1) { + if (setting1.description !== setting2.description || setting1.default !== setting2.default) { + isChanged = true; + return; + } + } + + if (setting1.type === 2) { + if ( + setting1.description !== setting2.description || + setting1.default !== setting2.default || + setting1.enabled !== setting2.enabled || + setting1.lowerLimit !== setting2.lowerLimit || + setting1.upperLimit !== setting2.upperLimit || + setting1.ticker !== setting2.ticker + ) { + isChanged = true; + return; + } + } + + if (setting1.type === 3) { + if ( + setting1.description !== setting2.description || + !compareObject(setting1.default, setting2.default) || + setting1.enabled !== setting2.enabled || + setting1.lowerLimit !== setting2.lowerLimit || + setting1.upperLimit !== setting2.upperLimit || + setting1.ticker !== setting2.ticker + ) { + isChanged = true; + return; + } + } + }); + + return isChanged; + }, [newSettings, channelSettings]); + + return ( + + {errorMessage && ( + + )} + + + + + + + {pushApprovalAmount >= EDIT_SETTING_FEE ? ( + + ) : ( + + )} + + + ); +}; + +export { NotificationSettingsFooter }; diff --git a/src/modules/notifSettings/components/NotificationSettingsHeader.tsx b/src/modules/notifSettings/components/NotificationSettingsHeader.tsx new file mode 100644 index 0000000000..545b2e1470 --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsHeader.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react'; + +import { Add, Box, Button, Text } from 'blocks'; + +import { ModalResponse } from 'common'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; +import { settingInitialValue } from '../NotificationSettings.constants'; +import { useAccount } from 'hooks'; + +type NotificationSettingsHeaderProps = { + modalControl: ModalResponse; + setSettingsToEdit: (settingsToEdit: ChannelSetting) => void; +}; +const NotificationSettingsHeader: FC = ({ modalControl, setSettingsToEdit }) => { + const { open } = modalControl; + const { isWalletConnected, connect } = useAccount(); + + const openModal = () => (!isWalletConnected ? connect() : open()); + + return ( + + + + Notification Settings + + + Add, Edit or Remove Notification Settings + + + + + ); +}; + +export { NotificationSettingsHeader }; diff --git a/src/modules/notifSettings/components/NotificationSettingsListItem.tsx b/src/modules/notifSettings/components/NotificationSettingsListItem.tsx new file mode 100644 index 0000000000..a2672097fc --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsListItem.tsx @@ -0,0 +1,85 @@ +import { FC } from 'react'; + +import { Box, Dropdown, KebabMenuVertical, Lozenge, Menu, MenuItem, OptOut, Pencil, Skeleton, Text } from 'blocks'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +import { ModalResponse } from 'common'; + +type NotificationSettingsListItemProps = { + setting: ChannelSetting; + loadingSettings: boolean; + modalControl: ModalResponse; + setSettingsToEdit: (settingsToEdit: ChannelSetting) => void; + handleDeleteSetting: (settingToDelete: ChannelSetting) => void; + handleSettingsChange: (setting: ChannelSetting) => void; +}; + +const NotificationSettingsListItem: FC = ({ + setting, + modalControl, + loadingSettings, + setSettingsToEdit, + handleDeleteSetting, +}) => { + const { open } = modalControl; + + return ( + + + + + {setting.description} + + {setting.type == 2 && Range} + {setting.type == 3 && Multi-Range} + + + + } + onClick={() => { + setSettingsToEdit(setting); + open(); + }} + /> + } + onClick={() => { + handleDeleteSetting(setting); + }} + /> + + } + > + + + + + + ); +}; + +export { NotificationSettingsListItem }; diff --git a/src/modules/notifSettings/components/NotificationSettingsLists.tsx b/src/modules/notifSettings/components/NotificationSettingsLists.tsx new file mode 100644 index 0000000000..b8d68b57e7 --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsLists.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; + +import { Box, Separator } from 'blocks'; + +import { ModalResponse } from 'common'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +import { NotificationSettingsListItem } from './NotificationSettingsListItem'; + +type NotificationSettingsListsProps = { + newSettings: ChannelSetting[]; + loadingSettings: boolean; + modalControl: ModalResponse; + setSettingsToEdit: (settingsToEdit: ChannelSetting) => void; + handleDeleteSetting: (settingToDelete: ChannelSetting) => void; + handleSettingsChange: (setting: ChannelSetting) => void; +}; + +const NotificationSettingsLists: FC = ({ + newSettings, + loadingSettings, + modalControl, + setSettingsToEdit, + handleDeleteSetting, + handleSettingsChange, +}) => { + return ( + + {newSettings.map((setting: ChannelSetting, index: number) => { + return ( + + + + + ); + })} + + ); +}; + +export { NotificationSettingsLists }; diff --git a/src/modules/notifSettings/components/NotificationSettingsModal.tsx b/src/modules/notifSettings/components/NotificationSettingsModal.tsx new file mode 100644 index 0000000000..32928f87c0 --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsModal.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; + +import { ModalResponse } from 'common'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +import { EditNotificationSettingsFormProvider } from '../EditNotificationSetting.form'; +import { NotificationSettingsModalWrapper } from './NotificationSettingsModalWrapper'; + +type NotificationSettingsModalProps = { + modalControl: ModalResponse; + settingsToEdit: ChannelSetting; + setNewSettings: (setting: ChannelSetting[]) => void; + handleSettingsChange: (setting: ChannelSetting) => void; +}; +const NotificationSettingsModal: FC = ({ + modalControl, + settingsToEdit, + handleSettingsChange, +}) => { + const { isOpen, onClose } = modalControl; + + return ( + { + // handleAddSettings(values); + }} + > + + + ); +}; + +export { NotificationSettingsModal }; diff --git a/src/modules/notifSettings/components/NotificationSettingsModalWrapper.tsx b/src/modules/notifSettings/components/NotificationSettingsModalWrapper.tsx new file mode 100644 index 0000000000..8ffa2e770f --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsModalWrapper.tsx @@ -0,0 +1,106 @@ +import { FC } from 'react'; + +import { Modal } from 'blocks'; + +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +import { AddNotificationSettingsModalContent } from './AddNotificationSettingsModalContent'; +import { useEditNotificationSettingsForm } from '../EditNotificationSetting.form'; + +type NotificationSettingsModalWrapperProps = { + isOpen: boolean; + onClose: () => void; + settingsToEdit: ChannelSetting; + handleSettingsChange: (setting: ChannelSetting) => void; +}; + +const NotificationSettingsModalWrapper: FC = ({ + isOpen, + onClose, + settingsToEdit, + handleSettingsChange, +}) => { + const { values: formValues, validateForm, setTouched, dirty, resetForm } = useEditNotificationSettingsForm(); + + const handleAddSettings = async (values: NotificationSettingFormValues) => { + setTouched({ + settingName: true, + defaultValue: true, + rangelowerlimit: true, + rangeupperlimit: true, + multirangelowerlimit: true, + multirangeupperlimit: true, + sliderStepValue: true, + enableRange: true, + enableMultiRange: true, + isDefault: true, + }); + + if (dirty) { + const validationErrors = await validateForm(); + + if (Object.keys(validationErrors).length > 0) { + return; + } + } + + const index = settingsToEdit.index !== 0 ? settingsToEdit.index : Math.floor(Math.random() * 1000000); + + const newAddedSettings: ChannelSetting = values.enableRange + ? values.enableMultiRange + ? { + type: 3, + default: { + lower: Number(values.multirangelowerlimit), + upper: Number(values.multirangeupperlimit), + }, + enabled: values.isDefault, + description: values.settingName, + lowerLimit: Number(values.rangelowerlimit), + upperLimit: Number(values.rangeupperlimit), + ticker: Number(values.sliderStepValue), + index: index, + } + : { + type: 2, + default: Number(values.defaultValue), + enabled: values.isDefault, + description: values.settingName, + lowerLimit: Number(values.rangelowerlimit), + upperLimit: Number(values.rangeupperlimit), + ticker: Number(values.sliderStepValue), + index: index, + } + : { + type: 1, + default: values.isDefault, + description: values.settingName, + index: index, + }; + + handleSettingsChange(newAddedSettings); + resetForm(); + + onClose(); + }; + + return ( + { + handleAddSettings(formValues); + }, + }} + cancelButtonProps={{ + children: 'Cancel', + }} + > + + + ); +}; + +export { NotificationSettingsModalWrapper }; diff --git a/src/modules/notifSettings/components/NotificationSettingsRangeSelector.tsx b/src/modules/notifSettings/components/NotificationSettingsRangeSelector.tsx new file mode 100644 index 0000000000..1b3a0574a1 --- /dev/null +++ b/src/modules/notifSettings/components/NotificationSettingsRangeSelector.tsx @@ -0,0 +1,241 @@ +import { useMemo, useState } from 'react'; + +import { Box, Text, TextInput } from 'blocks'; + +import Checkbox from 'components/reusables/checkbox/Checkbox'; + +import InputSlider from 'components/reusables/sliders/InputSlider'; +import RangeSlider from 'components/reusables/sliders/RangeSlider'; + +import { useEditNotificationSettingsForm } from '../EditNotificationSetting.form'; + +const NotificationSettingsRangeSelector = () => { + const { values: formValues, handleChange, setFieldValue, errors, touched } = useEditNotificationSettingsForm(); + + const showPreview = useMemo(() => { + const { + rangelowerlimit, + rangeupperlimit, + enableMultiRange, + multirangelowerlimit, + multirangeupperlimit, + defaultValue, + sliderStepValue, + } = formValues; + + const isRangeValid = Number(rangelowerlimit) > 0 && Number(rangeupperlimit) > 0; + const isSliderStepValid = + Number(sliderStepValue) > 0 && Number(sliderStepValue) <= Number(rangeupperlimit) - Number(rangelowerlimit); + + const isDefaultValid = + !enableMultiRange && + Number(defaultValue) >= Number(rangelowerlimit) && + Number(defaultValue) <= Number(rangeupperlimit); + + const isMultiRangeValid = + enableMultiRange && + Number(multirangelowerlimit) >= Number(rangelowerlimit) && + Number(multirangeupperlimit) <= Number(rangeupperlimit) && + Number(multirangeupperlimit) > Number(multirangelowerlimit); + + return isRangeValid && isSliderStepValid && (isMultiRangeValid || isDefaultValid); + }, [formValues]); + + const [sliderPreviewVal, setSliderPreviewVal] = useState(formValues.defaultValue); + const [sliderPreviewStartVal, setSliderPreviewStartVal] = useState(formValues.multirangelowerlimit); + const [sliderPreviewEndVal, setSliderPreviewEndVal] = useState(formValues.multirangeupperlimit); + + return ( + <> + + Range Values + + + to + { + setFieldValue('rangeupperlimit', e.target.value); + }} + error={touched.rangeupperlimit && Boolean(errors.rangeupperlimit)} + errorMessage={touched.rangeupperlimit ? errors.rangeupperlimit : ''} + /> + + + + + setFieldValue('enableMultiRange', !formValues.enableMultiRange)} + /> + + + Enable Multi Range + + User can select a range of values in the slider + + + + {!formValues.enableMultiRange && ( + { + setSliderPreviewVal(Number(e.target.value)); + setFieldValue('defaultValue', e.target.value); + }} + label="Default Value" + error={touched.defaultValue && Boolean(errors.defaultValue)} + errorMessage={touched.defaultValue ? errors.defaultValue : ''} + /> + )} + + {formValues.enableMultiRange && ( + + Range Values + + { + setFieldValue('multirangelowerlimit', e.target.value); + setSliderPreviewStartVal(Number(e.target.value)); + }} + error={touched.multirangelowerlimit && Boolean(errors.multirangelowerlimit)} + errorMessage={touched.multirangelowerlimit ? errors.multirangelowerlimit : ''} + /> + to + { + setFieldValue('multirangeupperlimit', e.target.value); + setSliderPreviewEndVal(Number(e.target.value)); + }} + error={touched.multirangeupperlimit && Boolean(errors.multirangeupperlimit)} + errorMessage={touched.multirangeupperlimit ? errors.multirangeupperlimit : ''} + /> + + + )} + + + { + // handleChange('sliderStepValue'); + // setFieldValue('sliderStepValue', e.target.value); + // }} + onChange={handleChange('sliderStepValue')} + label="Slider Step Value" + error={touched.sliderStepValue && Boolean(errors.sliderStepValue)} + errorMessage={touched.sliderStepValue ? errors.sliderStepValue : ''} + /> + + + {showPreview && ( + + Preview + + {!formValues.enableMultiRange && ( + + {formValues.rangelowerlimit} + + { + setSliderPreviewVal(x); + }} + preview={true} + /> + + {formValues.rangeupperlimit} + + )} + + {formValues.enableMultiRange && ( + + {formValues.rangelowerlimit} + + { + setSliderPreviewStartVal(startVal); + setSliderPreviewEndVal(endVal); + }} + preview={true} + /> + + {formValues.rangeupperlimit} + + )} + + )} + + ); +}; + +export { NotificationSettingsRangeSelector }; diff --git a/src/pages/NotificationSettingsPage.tsx b/src/pages/NotificationSettingsPage.tsx new file mode 100644 index 0000000000..b96727078a --- /dev/null +++ b/src/pages/NotificationSettingsPage.tsx @@ -0,0 +1,12 @@ +import { ContentLayout } from 'common'; +import { NotificationSettings } from 'modules/notifSettings/NotificationSettings'; + +const NotificationSettingsPage = () => { + return ( + + + + ); +}; + +export default NotificationSettingsPage; diff --git a/src/queries/hooks/index.ts b/src/queries/hooks/index.ts index 6442083f4e..af7cf2c265 100644 --- a/src/queries/hooks/index.ts +++ b/src/queries/hooks/index.ts @@ -4,3 +4,4 @@ export * from './user'; export * from './rewards'; export * from './pointsVault'; export * from './analytics'; +export * from './notificationSettings'; diff --git a/src/queries/hooks/notificationSettings/index.ts b/src/queries/hooks/notificationSettings/index.ts new file mode 100644 index 0000000000..77a2c6839a --- /dev/null +++ b/src/queries/hooks/notificationSettings/index.ts @@ -0,0 +1 @@ +export * from './useCreateNotificationSettings'; diff --git a/src/queries/hooks/notificationSettings/useCreateNotificationSettings.ts b/src/queries/hooks/notificationSettings/useCreateNotificationSettings.ts new file mode 100644 index 0000000000..6bd274a233 --- /dev/null +++ b/src/queries/hooks/notificationSettings/useCreateNotificationSettings.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { createNotificationSettings } from 'queries/queryKeys'; +import { addNotificationSettings } from 'queries/services'; + +export const useCreateNotificationSettings = () => + useMutation({ + mutationKey: [createNotificationSettings], + mutationFn: addNotificationSettings, + }); diff --git a/src/queries/queryKeys.ts b/src/queries/queryKeys.ts index ec74ac79f5..accea9bf0b 100644 --- a/src/queries/queryKeys.ts +++ b/src/queries/queryKeys.ts @@ -1,5 +1,6 @@ export const UserRewardsDetails = 'userRewardsDetails'; export const addDelegate = 'addDelegate'; +export const createNotificationSettings = 'createNotificationSettings'; export const addNewSubgraph = 'addNewSubgraph'; export const aliasInfo = 'aliasInfo'; export const allActivities = 'allActivities'; diff --git a/src/queries/services/index.ts b/src/queries/services/index.ts index 18dae290cf..632b38dfaa 100644 --- a/src/queries/services/index.ts +++ b/src/queries/services/index.ts @@ -4,3 +4,4 @@ export * from './rewards'; export * from './pointsVault'; export * from './createChannel'; export * from './analytics'; +export * from './notificationSettings'; diff --git a/src/queries/services/notificationSettings/addNotificationSettings.ts b/src/queries/services/notificationSettings/addNotificationSettings.ts new file mode 100644 index 0000000000..e4acb2d0c4 --- /dev/null +++ b/src/queries/services/notificationSettings/addNotificationSettings.ts @@ -0,0 +1,8 @@ +import { AddNotificationSettingsProps } from 'queries/types'; + +export const addNotificationSettings = async ({ + userPushSDKInstance, + settings, +}: AddNotificationSettingsProps): Promise<{ + transactionHash: any; +}> => await userPushSDKInstance.channel.setting(settings); diff --git a/src/queries/services/notificationSettings/index.ts b/src/queries/services/notificationSettings/index.ts new file mode 100644 index 0000000000..6036f70337 --- /dev/null +++ b/src/queries/services/notificationSettings/index.ts @@ -0,0 +1 @@ +export * from './addNotificationSettings'; diff --git a/src/queries/types/index.ts b/src/queries/types/index.ts index 5f30e3836e..a99590a6c3 100644 --- a/src/queries/types/index.ts +++ b/src/queries/types/index.ts @@ -3,3 +3,4 @@ export * from './user'; export * from './rewards'; export * from './pointsVault'; export * from './createChannel'; +export * from './notificationsettings'; diff --git a/src/queries/types/notificationsettings.ts b/src/queries/types/notificationsettings.ts new file mode 100644 index 0000000000..7dfb3977e8 --- /dev/null +++ b/src/queries/types/notificationsettings.ts @@ -0,0 +1,6 @@ +import { ChannelSetting } from 'modules/channelDashboard/ChannelDashboard.types'; + +export type AddNotificationSettingsProps = { + userPushSDKInstance: any; + settings: ChannelSetting[]; +}; diff --git a/src/structure/MasterInterfacePage.tsx b/src/structure/MasterInterfacePage.tsx index aa796d0cf3..3aa5d59373 100644 --- a/src/structure/MasterInterfacePage.tsx +++ b/src/structure/MasterInterfacePage.tsx @@ -33,6 +33,7 @@ const NotAvailablePage = lazy(() => import('pages/NotAvailablePage')); const NotFoundPage = lazy(() => import('pages/NotFoundPage')); const ReceiveNotifsPage = lazy(() => import('pages/ReceiveNotifsPage')); const NotifSettingsPage = lazy(() => import('pages/NotifSettingsPage')); +const NotificationSettingsPage = lazy(() => import('pages/NotificationSettingsPage')); const SendNotifsPage = lazy(() => import('pages/SendNotifsPage')); const SpacePage = lazy(() => import('pages/SpacePage')); const SupportPage = lazy(() => import('pages/SupportPage')); @@ -328,9 +329,13 @@ function MasterInterfacePage() { path={APP_PATHS.UserSettings} element={} /> - } + /> */} + } />