diff --git a/package-lock.json b/package-lock.json index dfe35408..43361a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@deriv-com/analytics": "^1.16.1", "@deriv-com/api-hooks": "^1.4.9", "@deriv-com/translations": "^1.3.7", - "@deriv-com/ui": "^1.29.0", + "@deriv-com/ui": "^1.35.5", "@deriv-com/utils": "^0.0.28", "@deriv/quill-icons": "^1.23.8", "@sendbird/chat": "^4.11.3", @@ -3892,21 +3892,23 @@ ] }, "node_modules/@deriv-com/ui": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.29.0.tgz", - "integrity": "sha512-AbeUDhHLzY9hFzi6SE4/otFnea/yoUtmoK7uiTn7P28k9esNzeRfywO+TUL18SX3suCJbhMqGp5GtGa57//kAw==", + "version": "1.35.5", + "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.35.5.tgz", + "integrity": "sha512-v6eKPhpjPt8Z8OvOEC6sheCFTcO6Xmiexps+kXd8+/RwzqIugeU+SjhF9MRy2V396cKBzt/ph9zumZUnNm/Kew==", "dependencies": { + "@popperjs/core": "^2.11.8", "@types/react-modal": "^3.16.3", - "react-tiny-popover": "^8.0.4" + "lodash.debounce": "^4.0.8", + "react-popper": "^2.3.0" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.13.0" } }, "node_modules/@deriv-com/ui/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", "cpu": [ "x64" ], @@ -18099,6 +18101,20 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -18239,15 +18255,6 @@ } } }, - "node_modules/react-tiny-popover": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/react-tiny-popover/-/react-tiny-popover-8.0.4.tgz", - "integrity": "sha512-pn0Y/G0gyMdYTBEWSKCCnaZsXAa54PkfnRE4fnMM5633SSClYrXxwXKc6vPYgJ9shLatGginxMjnhXq6guZmng==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 1fd7fdc9..181612d3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@deriv-com/analytics": "^1.16.1", "@deriv-com/api-hooks": "^1.4.9", "@deriv-com/translations": "^1.3.7", - "@deriv-com/ui": "^1.29.0", + "@deriv-com/ui": "^1.35.5", "@deriv-com/utils": "^0.0.28", "@deriv/quill-icons": "^1.23.8", "@sendbird/chat": "^4.11.3", @@ -116,4 +116,4 @@ "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5" } -} +} \ No newline at end of file diff --git a/src/components/AdvertiserName/AdvertiserNameStats.scss b/src/components/AdvertiserName/AdvertiserNameStats.scss index 77b412a2..7d1c156c 100644 --- a/src/components/AdvertiserName/AdvertiserNameStats.scss +++ b/src/components/AdvertiserName/AdvertiserNameStats.scss @@ -9,7 +9,7 @@ gap: 1rem; } - & div:not(.deriv-tooltip-container):not(.deriv-tooltip-container *) { + & div:not(.deriv-tooltip) { display: flex; align-items: center; gap: 0.8rem; @@ -27,7 +27,7 @@ } @include desktop { &:nth-child(2) { - & div { + & div:not(.deriv-tooltip) { padding-right: 0; } } diff --git a/src/components/AdvertiserName/AdvertiserNameStats.tsx b/src/components/AdvertiserName/AdvertiserNameStats.tsx index 4c9b29bf..a4b7513b 100644 --- a/src/components/AdvertiserName/AdvertiserNameStats.tsx +++ b/src/components/AdvertiserName/AdvertiserNameStats.tsx @@ -5,9 +5,8 @@ import { useModalManager } from '@/hooks'; import { getCurrentRoute } from '@/utils'; import { LabelPairedThumbsUpSmRegularIcon } from '@deriv/quill-icons'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { Text, useDevice } from '@deriv-com/ui'; +import { Text, Tooltip, useDevice } from '@deriv-com/ui'; import { BlockUserCountModal } from '../Modals'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; import BlockUserCount from './BlockUserCount'; import './AdvertiserNameStats.scss'; @@ -99,9 +98,10 @@ const AdvertiserNameStats = ({ advertiserStats }: { advertiserStats: DeepPartial
- { isDesktop ? undefined : showModal('BlockUserCountModal'); }} @@ -111,7 +111,7 @@ const AdvertiserNameStats = ({ advertiserStats }: { advertiserStats: DeepPartial {recommendedAverage || 0}% - +
)} diff --git a/src/components/AdvertiserName/BlockDropdown.tsx b/src/components/AdvertiserName/BlockDropdown.tsx index 3aacc44d..db59241c 100644 --- a/src/components/AdvertiserName/BlockDropdown.tsx +++ b/src/components/AdvertiserName/BlockDropdown.tsx @@ -18,7 +18,7 @@ const BlockDropdown = ({ id, onClickBlocked }: TBlockDropdownProps) => { return (
} + chevronIcon={} list={[ { text: localize('Block'), diff --git a/src/components/AdvertiserName/BlockUserCount.scss b/src/components/AdvertiserName/BlockUserCount.scss index 23d241ad..310e692f 100644 --- a/src/components/AdvertiserName/BlockUserCount.scss +++ b/src/components/AdvertiserName/BlockUserCount.scss @@ -1,9 +1,4 @@ .block-user-count { - & .deriv-tooltip-container { - display: flex; - align-items: center; - gap: 0.1rem; - } &__tooltip { padding: 0 0.8rem; display: flex; @@ -28,4 +23,12 @@ &__button { padding: 0; } + + & .deriv-tooltip { + padding: 0.8rem; + + @include mobile-or-tablet-screen { + display: none; + } + } } diff --git a/src/components/AdvertiserName/BlockUserCount.tsx b/src/components/AdvertiserName/BlockUserCount.tsx index 904e4013..8efe1fd5 100644 --- a/src/components/AdvertiserName/BlockUserCount.tsx +++ b/src/components/AdvertiserName/BlockUserCount.tsx @@ -2,9 +2,8 @@ import { TLocalize } from 'types'; import { useModalManager } from '@/hooks'; import { LabelPairedCircleUserSlashSmRegularIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { Text, useDevice } from '@deriv-com/ui'; +import { Text, Tooltip, useDevice } from '@deriv-com/ui'; import { BlockUserCountModal } from '../Modals'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; import './BlockUserCount.scss'; type TBlockUserCount = { @@ -32,21 +31,21 @@ const BlockUserCount = ({ count }: TBlockUserCount) => { const { isDesktop, isMobile } = useDevice(); return (
- { isDesktop ? undefined : showModal('BlockUserCountModal'); }} - tooltipContent={getMessage(localize, count)} + tooltipContent={{getMessage(localize, count)}} > {count ?? 0} - + {!!isModalOpenFor('BlockUserCountModal') && ( )} diff --git a/src/components/AdvertsTableRow/AdvertsTableRow.tsx b/src/components/AdvertsTableRow/AdvertsTableRow.tsx index 9c15eb7c..74c90474 100644 --- a/src/components/AdvertsTableRow/AdvertsTableRow.tsx +++ b/src/components/AdvertsTableRow/AdvertsTableRow.tsx @@ -218,7 +218,7 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { className='lg:min-w-[7.5rem]' disabled={isAdvertiserBarred} onClick={() => { - if (!isPoaVerified || !isPoiVerified) { + if (!isAdvertiser && (!isPoaVerified || !isPoiVerified)) { const searchParams = new URLSearchParams(location.search); searchParams.set('poi_poa_verified', 'false'); history.replace({ @@ -226,7 +226,7 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { search: searchParams.toString(), }); } else { - if (!isAdvertiser) setSelectedAdvertId(advertId); + setSelectedAdvertId(advertId); showModal(isAdvertiser ? 'BuySellForm' : 'NicknameModal'); } }} diff --git a/src/components/AppFooter/AccountLimits.tsx b/src/components/AppFooter/AccountLimits.tsx index c2282e54..482debe0 100644 --- a/src/components/AppFooter/AccountLimits.tsx +++ b/src/components/AppFooter/AccountLimits.tsx @@ -1,20 +1,15 @@ import { ACCOUNT_LIMITS } from '@/constants'; import { LegacyAccountLimitsIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const AccountLimits = () => { const { localize } = useTranslations(); return ( - + - + ); }; diff --git a/src/components/AppFooter/ChangeTheme.tsx b/src/components/AppFooter/ChangeTheme.tsx index 530cfdf3..0eb5c633 100644 --- a/src/components/AppFooter/ChangeTheme.tsx +++ b/src/components/AppFooter/ChangeTheme.tsx @@ -1,6 +1,6 @@ import { LegacyThemeLightIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const ChangeTheme = () => { const { localize } = useTranslations(); @@ -8,9 +8,9 @@ const ChangeTheme = () => { return ( // TODO need to add theme logic // TODO update the component's tests after adding the logic - + - + ); }; diff --git a/src/components/AppFooter/Deriv.tsx b/src/components/AppFooter/Deriv.tsx index 643556d0..b072bd65 100644 --- a/src/components/AppFooter/Deriv.tsx +++ b/src/components/AppFooter/Deriv.tsx @@ -1,13 +1,13 @@ import { DERIV_COM } from '@/constants'; import { LegacyDerivIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const Deriv = () => { const { localize } = useTranslations(); return ( - { tooltipContent={localize('Go to deriv.com')} > - + ); }; diff --git a/src/components/AppFooter/FullScreen.tsx b/src/components/AppFooter/FullScreen.tsx index c1e0da75..edbbdc3e 100644 --- a/src/components/AppFooter/FullScreen.tsx +++ b/src/components/AppFooter/FullScreen.tsx @@ -1,21 +1,21 @@ import { useFullScreen } from '@/hooks'; import { LegacyFullscreen1pxIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const FullScreen = () => { const { toggleFullScreenMode } = useFullScreen(); const { localize } = useTranslations(); return ( - - + ); }; diff --git a/src/components/AppFooter/HelpCentre.tsx b/src/components/AppFooter/HelpCentre.tsx index 350ff9be..85c7ba57 100644 --- a/src/components/AppFooter/HelpCentre.tsx +++ b/src/components/AppFooter/HelpCentre.tsx @@ -1,13 +1,13 @@ import { HELP_CENTRE } from '@/constants'; import { LegacyHelpCentreIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const HelpCentre = () => { const { localize } = useTranslations(); return ( - { tooltipContent={localize('Help centre')} > - + ); }; diff --git a/src/components/AppFooter/LanguageSettings.tsx b/src/components/AppFooter/LanguageSettings.tsx index d8c98356..d1e4cee8 100644 --- a/src/components/AppFooter/LanguageSettings.tsx +++ b/src/components/AppFooter/LanguageSettings.tsx @@ -1,9 +1,8 @@ import { useMemo } from 'react'; import { LANGUAGES } from '@/constants'; import { useTranslations } from '@deriv-com/translations'; -import { Text } from '@deriv-com/ui'; +import { Text, Tooltip } from '@deriv-com/ui'; import { LocalStorageUtils } from '@deriv-com/utils'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; type TLanguageSettings = { openLanguageSettingModal: () => void; @@ -19,7 +18,7 @@ const LanguageSettings = ({ openLanguageSettingModal }: TLanguageSettings) => { ); return ( - { {currentLang} - + ); }; diff --git a/src/components/AppFooter/Livechat.tsx b/src/components/AppFooter/Livechat.tsx index 08848cd8..2a6ae165 100644 --- a/src/components/AppFooter/Livechat.tsx +++ b/src/components/AppFooter/Livechat.tsx @@ -1,14 +1,14 @@ import { useLiveChat } from '@/hooks/custom-hooks'; import { LegacyLiveChatOutlineIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const Livechat = () => { const { LiveChatWidget } = useLiveChat(); const { localize } = useTranslations(); return ( - { @@ -17,7 +17,7 @@ const Livechat = () => { tooltipContent={localize('Live chat')} > - + ); }; diff --git a/src/components/AppFooter/NetworkStatus.tsx b/src/components/AppFooter/NetworkStatus.tsx index 5468bc1d..6e2680ec 100644 --- a/src/components/AppFooter/NetworkStatus.tsx +++ b/src/components/AppFooter/NetworkStatus.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import clsx from 'clsx'; import { useNetworkStatus } from '@/hooks'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const statusConfigs = { blinking: { @@ -20,15 +20,14 @@ const NetworkStatus = () => { const { className, tooltip } = useMemo(() => statusConfigs[status], [status]); return ( -
- + ); }; diff --git a/src/components/AppFooter/ResponsibleTrading.tsx b/src/components/AppFooter/ResponsibleTrading.tsx index a9449baf..9c87029b 100644 --- a/src/components/AppFooter/ResponsibleTrading.tsx +++ b/src/components/AppFooter/ResponsibleTrading.tsx @@ -1,13 +1,13 @@ import { RESPONSIBLE } from '@/constants'; import { LegacyResponsibleTradingIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Tooltip } from '@deriv-com/ui'; const ResponsibleTrading = () => { const { localize } = useTranslations(); return ( - { tooltipContent={localize('Responsible trading')} > - + ); }; diff --git a/src/components/AppFooter/ServerTime.tsx b/src/components/AppFooter/ServerTime.tsx index 9972a6f9..1311fdba 100644 --- a/src/components/AppFooter/ServerTime.tsx +++ b/src/components/AppFooter/ServerTime.tsx @@ -1,8 +1,7 @@ import { DATE_TIME_FORMAT_WITH_GMT, DATE_TIME_FORMAT_WITH_OFFSET } from '@/constants'; import { useSyncedTime } from '@/hooks'; import { epochToLocal, epochToUTC } from '@/utils'; -import { Text, useDevice } from '@deriv-com/ui'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Text, Tooltip, useDevice } from '@deriv-com/ui'; const ServerTime = () => { const time = useSyncedTime(); @@ -11,15 +10,9 @@ const ServerTime = () => { const { isDesktop } = useDevice(); return ( - + {UTCFormat} - + ); }; diff --git a/src/components/AppFooter/WhatsApp.tsx b/src/components/AppFooter/WhatsApp.tsx index 1c2fe02d..9540bfa6 100644 --- a/src/components/AppFooter/WhatsApp.tsx +++ b/src/components/AppFooter/WhatsApp.tsx @@ -1,13 +1,13 @@ import { LegacyWhatsappIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; +import { Tooltip } from '@deriv-com/ui'; import { URLConstants } from '@deriv-com/utils'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; const WhatsApp = () => { const { localize } = useTranslations(); return ( - { tooltipContent={localize('WhatsApp')} > - + ); }; diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index 5fdca055..34ffe17f 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -1,13 +1,12 @@ import { useEffect } from 'react'; import { getOauthUrl } from '@/constants'; -import { api, useOAuth } from '@/hooks'; +import { api, useGrowthbookGetFeatureValue, useOAuth } from '@/hooks'; import { getCurrentRoute } from '@/utils'; import { StandaloneCircleUserRegularIcon } from '@deriv/quill-icons'; import { useAuthData } from '@deriv-com/api-hooks'; import { useTranslations } from '@deriv-com/translations'; -import { Button, Header, Text, useDevice, Wrapper } from '@deriv-com/ui'; +import { Button, Header, Text, Tooltip, useDevice, Wrapper } from '@deriv-com/ui'; import { LocalStorageUtils } from '@deriv-com/utils'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; import { AccountsInfoLoader } from './AccountsInfoLoader'; import { AccountSwitcher } from './AccountSwitcher'; import { AppLogo } from './AppLogo'; @@ -31,6 +30,9 @@ const AppHeader = () => { document.documentElement.dir = instance.dir((currentLang || 'en').toLowerCase()); }, [currentLang, instance]); const { oAuthLogout } = useOAuth(); + const [isNotificationServiceEnabled] = useGrowthbookGetFeatureValue({ + featureFlag: 'new_notifications_service_enabled', + }); const renderAccountSection = () => { if (!isEndpointPage && !activeAccount) { @@ -40,18 +42,17 @@ const AppHeader = () => { if (activeLoginid) { return ( <> - + {isNotificationServiceEnabled && } {isDesktop && ( - - + )} diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss deleted file mode 100644 index e85012aa..00000000 --- a/src/components/Input/Input.scss +++ /dev/null @@ -1,65 +0,0 @@ -.input { - display: flex; - justify-content: center; - margin: 0.5rem 0 3.6rem; - width: 100%; - - &__leading-icon { - position: absolute; - display: flex; - align-items: center; - margin: 0 1.6rem; - } - - @include mobile-or-tablet-screen { - flex-direction: column; - margin-bottom: 1rem; - } - - &__error { - padding-top: 0.2rem; - position: absolute; - margin-left: 1rem; - bottom: 13.3rem; - left: 4.5rem; - - @include mobile-or-tablet-screen { - bottom: 0; - box-decoration-break: clone; - padding-top: 0.4rem; - margin: 0 0 0.6rem 1.6rem; - margin-bottom: 0.6rem; - position: relative; - left: 0; - } - } - - &__field { - display: flex; - width: 92%; - height: 4rem; - padding: 1rem 1.6rem; - justify-content: center; - align-items: center; - border-radius: 4px; - border: 1px solid #d6dadb; - - @include mobile-or-tablet-screen { - width: 100%; - } - - &:focus-visible { - outline: none; - border-color: #85acb0; - } - - &::placeholder { - color: #999; - } - - &--error { - // stylelint-disable-next-line declaration-no-important - border-color: #ec3f3f !important; - } - } -} diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx deleted file mode 100644 index 72a1029b..00000000 --- a/src/components/Input/Input.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { forwardRef, ReactElement } from 'react'; -import clsx from 'clsx'; -import { Text, useDevice } from '@deriv-com/ui'; -import './Input.scss'; - -type TInputProps = { - errorMessage?: string; - hasError?: boolean; - leadingIcon?: ReactElement; - name: string; - onBlur?: () => void; - onChange?: () => void; - placeholder?: string; - value?: string; -}; - -const Input = forwardRef( - ({ errorMessage, hasError, leadingIcon, onBlur, onChange, placeholder, value, ...props }, ref) => { - const { isDesktop } = useDevice(); - - return ( -
- {leadingIcon &&
{leadingIcon}
} - - {hasError && ( - - {errorMessage} - - )} -
- ); - } -); - -Input.displayName = 'Input'; - -export default Input; diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts deleted file mode 100644 index b4d38647..00000000 --- a/src/components/Input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Input } from './Input'; diff --git a/src/components/Modals/AdCreateEditSuccessModal/AdCreateEditSuccessModal.tsx b/src/components/Modals/AdCreateEditSuccessModal/AdCreateEditSuccessModal.tsx index 2ef6ddaf..403dbbef 100644 --- a/src/components/Modals/AdCreateEditSuccessModal/AdCreateEditSuccessModal.tsx +++ b/src/components/Modals/AdCreateEditSuccessModal/AdCreateEditSuccessModal.tsx @@ -52,14 +52,14 @@ const AdCreateEditSuccessModal = ({ } + label={} name='ad-create-success-message' onChange={onToggleCheckbox} /> diff --git a/src/components/Modals/AdCreateEditSuccessModal/__tests__/AdCreateEditSuccessModal.spec.tsx b/src/components/Modals/AdCreateEditSuccessModal/__tests__/AdCreateEditSuccessModal.spec.tsx index ade25746..801e0087 100644 --- a/src/components/Modals/AdCreateEditSuccessModal/__tests__/AdCreateEditSuccessModal.spec.tsx +++ b/src/components/Modals/AdCreateEditSuccessModal/__tests__/AdCreateEditSuccessModal.spec.tsx @@ -46,7 +46,7 @@ describe('AdCreateEditSuccessModal', () => { }); it('should handle ok button click', async () => { render(); - const okButton = screen.getByRole('button', { name: 'Ok' }); + const okButton = screen.getByRole('button', { name: 'OK' }); expect(okButton).toBeInTheDocument(); await userEvent.click(okButton); expect(mockProps.onRequestClose).toBeCalledTimes(1); diff --git a/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx b/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx index 35d9d17b..10e961b8 100644 --- a/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx +++ b/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx @@ -1,7 +1,13 @@ -import { Dispatch, SetStateAction, useEffect } from 'react'; -import { api } from '@/hooks'; +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; +import { BUY_SELL_URL, ERROR_CODES } from '@/constants'; +import { api, useModalManager } from '@/hooks'; +import { useErrorStore } from '@/stores'; +import { getCurrentRoute } from '@/utils'; import { Localize, useTranslations } from '@deriv-com/translations'; import { Button, Modal, Text, useDevice } from '@deriv-com/ui'; +import { ErrorModal } from '../ErrorModal'; import './BlockUnblockUserModal.scss'; type TBlockUnblockUserModalProps = { @@ -11,7 +17,6 @@ type TBlockUnblockUserModalProps = { isModalOpen: boolean; onClickBlocked?: () => void; onRequestClose: () => void; - setErrorMessage?: Dispatch>; }; const BlockUnblockUserModal = ({ @@ -21,10 +26,9 @@ const BlockUnblockUserModal = ({ isModalOpen, onClickBlocked, onRequestClose, - setErrorMessage, }: TBlockUnblockUserModalProps) => { - const { isMobile } = useDevice(); const { localize } = useTranslations(); + const { isMobile } = useDevice(); const { mutate: blockAdvertiser, mutation: { error, isSuccess }, @@ -33,17 +37,28 @@ const BlockUnblockUserModal = ({ mutate: unblockAdvertiser, mutation: { error: unblockError, isSuccess: unblockIsSuccess }, } = api.counterparty.useUnblock(); + const { hideModal, isModalOpenFor, showModal } = useModalManager(); + const { errorMessages, setErrorMessages } = useErrorStore( + useShallow(state => ({ errorMessages: state.errorMessages, setErrorMessages: state.setErrorMessages })) + ); + const isAdvertiser = getCurrentRoute() === 'advertiser'; + const history = useHistory(); useEffect(() => { if (isSuccess || unblockIsSuccess) { onClickBlocked?.(); onRequestClose(); } else if (error || unblockError) { - setErrorMessage?.(error?.message || unblockError?.message); - onRequestClose(); + setErrorMessages(error || unblockError); + + if (error?.code === ERROR_CODES.PERMISSION_DENIED && isAdvertiser) { + showModal('ErrorModal'); + } else { + onRequestClose(); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess, onClickBlocked, unblockIsSuccess, unblockError, error, setErrorMessage]); + }, [isSuccess, onClickBlocked, unblockIsSuccess, unblockError, error, setErrorMessages]); const textSize = isMobile ? 'md' : 'sm'; const getModalTitle = () => @@ -70,6 +85,24 @@ const BlockUnblockUserModal = ({ } }; + const permissionDeniedError = errorMessages.find(error => error.code === ERROR_CODES.PERMISSION_DENIED); + + if (permissionDeniedError && isModalOpenFor('ErrorModal')) { + return ( + { + hideModal(); + history.push(BUY_SELL_URL); + }} + title={localize('Unable to block advertiser')} + /> + ); + } + return ( ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ push: mockPush }), +})); jest.mock('@/hooks', () => ({ ...jest.requireActual('@/hooks'), @@ -12,10 +36,7 @@ jest.mock('@/hooks', () => ({ counterparty: { useBlock: jest.fn(() => ({ mutate: mockUseBlockMutate, - mutation: { - error: {}, - isSuccess: false, - }, + mutation: mockBlockMutation, })), useUnblock: jest.fn(() => ({ mutate: mockUseUnblockMutate, @@ -28,11 +49,25 @@ jest.mock('@/hooks', () => ({ }, })); +jest.mock('@/hooks/custom-hooks', () => ({ + useModalManager: jest.fn(() => mockModalManager), +})); + +jest.mock('@/utils', () => ({ + getCurrentRoute: jest.fn().mockReturnValue('my-profile'), +})); + jest.mock('@deriv-com/ui', () => ({ ...jest.requireActual('@deriv-com/ui'), useDevice: jest.fn(() => ({ isMobile: false })), })); +jest.mock('@/stores', () => ({ + useErrorStore: jest.fn(selector => (selector ? selector(mockStore) : mockStore)), +})); + +const mockGetCurrentRoute = getCurrentRoute as jest.Mock; + describe('BlockUnblockUserModal', () => { it('should render the modal with correct title and behaviour for blocking user', async () => { render( @@ -100,4 +135,67 @@ describe('BlockUnblockUserModal', () => { expect(mockOnRequestClose).toBeCalled(); }); + + it('should call onClickBlocked and onRequestClose if isSuccess or mutation returns success', async () => { + mockBlockMutation.isSuccess = true; + const mockOnClickBlocked = jest.fn(); + render( + + ); + + expect(mockOnRequestClose).toHaveBeenCalled(); + expect(mockOnClickBlocked).toHaveBeenCalled(); + }); + + it('should show error modal when permission is denied and current route is advertiser', async () => { + mockGetCurrentRoute.mockReturnValue('advertiser'); + mockModalManager.isModalOpenFor.mockImplementation((modalName: string) => modalName === 'ErrorModal'); + const error = { + code: 'PermissionDenied', + message: 'You are not allowed to block this user', + }; + // @ts-expect-error - mock values + mockStore.errorMessages = [error]; + mockBlockMutation.error = error; + mockBlockMutation.isSuccess = false; + render( + + ); + + expect(screen.queryByText('Unable to block advertiser')).toBeVisible(); + expect(screen.queryByText('You are not allowed to block this user')).toBeVisible(); + }); + + it('should call hideModal and history.push when user clicks on Got it button', async () => { + render( + + ); + + const gotItBtn = screen.getByRole('button', { + name: 'Got it', + }); + await userEvent.click(gotItBtn); + + expect(mockModalManager.hideModal).toHaveBeenCalled(); + expect(mockPush).toHaveBeenCalledWith(BUY_SELL_URL); + }); }); diff --git a/src/components/Modals/DailyLimitModal/DailyLimitModal.tsx b/src/components/Modals/DailyLimitModal/DailyLimitModal.tsx index 18a86189..4fcc6ca1 100644 --- a/src/components/Modals/DailyLimitModal/DailyLimitModal.tsx +++ b/src/components/Modals/DailyLimitModal/DailyLimitModal.tsx @@ -12,6 +12,7 @@ type TDailyLimitModalProps = { const DailyLimitModal = ({ currency, isModalOpen, onRequestClose }: TDailyLimitModalProps) => { const { data, error, isPending: isLoading, isSuccess, mutate } = api.advertiser.useUpdate(); + const { mutate: updateNotification } = api.notification.useUpdate(); const { daily_buy_limit: dailyBuyLimit, daily_sell_limit: dailySellLimit } = data ?? {}; const { isDesktop } = useDevice(); const textSize = isDesktop ? 'sm' : 'md'; @@ -34,7 +35,16 @@ const DailyLimitModal = ({ currency, isModalOpen, onRequestClose }: TDailyLimitM > - diff --git a/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx b/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx index 9d46c8bd..ca802f9a 100644 --- a/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx +++ b/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx @@ -21,6 +21,9 @@ jest.mock('@/hooks', () => ({ advertiser: { useUpdate: jest.fn(() => mockUseAdvertiserUpdate), }, + notification: { + useUpdate: jest.fn(() => ({ mutate: jest.fn() })), + }, }, })); diff --git a/src/components/Modals/OrderTimeTooltipModal/OrderTimeTooltipModal.tsx b/src/components/Modals/OrderTimeTooltipModal/OrderTimeTooltipModal.tsx index 6a75eada..e59dc288 100644 --- a/src/components/Modals/OrderTimeTooltipModal/OrderTimeTooltipModal.tsx +++ b/src/components/Modals/OrderTimeTooltipModal/OrderTimeTooltipModal.tsx @@ -19,7 +19,7 @@ const OrderTimeTooltipModal = ({ isModalOpen, onRequestClose }: TOrderTimeToolti diff --git a/src/components/Modals/OrderTimeTooltipModal/__tests__/OrderTimeTooltipModal.spec.tsx b/src/components/Modals/OrderTimeTooltipModal/__tests__/OrderTimeTooltipModal.spec.tsx index 00b39c7b..bc7b3bc6 100644 --- a/src/components/Modals/OrderTimeTooltipModal/__tests__/OrderTimeTooltipModal.spec.tsx +++ b/src/components/Modals/OrderTimeTooltipModal/__tests__/OrderTimeTooltipModal.spec.tsx @@ -14,7 +14,7 @@ describe('', () => { }); it('should handle the onclick for ok button', async () => { render(); - const okButton = screen.getByRole('button', { name: 'Ok' }); + const okButton = screen.getByRole('button', { name: 'OK' }); await userEvent.click(okButton); expect(mockProps.onRequestClose).toBeCalledTimes(1); }); diff --git a/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.scss b/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.scss index 9f1c5b37..0a4498e4 100644 --- a/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.scss +++ b/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.scss @@ -21,6 +21,7 @@ width: 12.8rem; border: 0; padding: 0; + margin-top: unset; } .deriv-input { diff --git a/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.tsx b/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.tsx index c4cc1fa9..6d939b8d 100644 --- a/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.tsx +++ b/src/components/PaymentMethodCard/PaymentMethodCardHeader/PaymentMethodCardHeader.tsx @@ -61,8 +61,8 @@ const PaymentMethodCardHeader = ({ /> {isEditable && ( } className='payment-method-card__header-dropdown' - dropdownIcon={} list={getActions(localize)} name='payment-method-actions' onSelect={value => { diff --git a/src/components/PaymentMethodForm/PaymentMethodFormAutocomplete/PaymentMethodFormAutocomplete.tsx b/src/components/PaymentMethodForm/PaymentMethodFormAutocomplete/PaymentMethodFormAutocomplete.tsx index e1aacfad..76edfbcd 100644 --- a/src/components/PaymentMethodForm/PaymentMethodFormAutocomplete/PaymentMethodFormAutocomplete.tsx +++ b/src/components/PaymentMethodForm/PaymentMethodFormAutocomplete/PaymentMethodFormAutocomplete.tsx @@ -1,3 +1,4 @@ +import { MouseEvent } from 'react'; import { TFormState, THooks, TPaymentMethod, TSelectedPaymentMethod } from 'types'; import { LabelPairedSearchMdRegularIcon, LegacyCloseCircle1pxBlackIcon } from '@deriv/quill-icons'; import { Localize, useTranslations } from '@deriv-com/translations'; @@ -52,27 +53,34 @@ const PaymentMethodFormAutocomplete = ({ } return ''; }; + + const handleDropdownClick = (event: MouseEvent) => { + event.preventDefault(); + }; return ( <> - } - isFullWidth - label={localize('Payment method')} - list={availablePaymentMethodsList} - name='Payment method' - onSelect={value => { - const selectedPaymentMethod = availablePaymentMethods?.find(p => p.id === value); - if (selectedPaymentMethod) { - onAdd?.({ - displayName: selectedPaymentMethod?.display_name, - fields: selectedPaymentMethod?.fields, - method: selectedPaymentMethod?.id, - }); - } - }} - value={getValue()} - variant='prompt' - /> + {/* To prevent default submission */} +
+ } + isFullWidth + label={localize('Payment method')} + list={availablePaymentMethodsList} + name='Payment method' + onSelect={value => { + const selectedPaymentMethod = availablePaymentMethods?.find(p => p.id === value); + if (selectedPaymentMethod) { + onAdd?.({ + displayName: selectedPaymentMethod?.display_name, + fields: selectedPaymentMethod?.fields, + method: selectedPaymentMethod?.id, + }); + } + }} + value={getValue()} + variant='prompt' + /> +
diff --git a/src/components/PopoverDropdown/PopoverDropdown.scss b/src/components/PopoverDropdown/PopoverDropdown.scss index cb20731c..b567a535 100644 --- a/src/components/PopoverDropdown/PopoverDropdown.scss +++ b/src/components/PopoverDropdown/PopoverDropdown.scss @@ -2,18 +2,26 @@ display: flex; flex-direction: column; position: relative; + justify-content: center; + padding: 0.1rem 0.8rem; - & .tooltip-menu-icon { - width: 3.2rem; - display: flex; - justify-content: center; - border-radius: 4px; + @include desktop { + width: 4.2rem; + } + + @include mobile-or-tablet-screen { + margin-top: -1rem; + } - @include mobile-or-tablet-screen { - margin-top: -0.5rem; + @include desktop { + & .deriv-tooltip__trigger { + display: flex; + width: 100%; + justify-content: center; &:hover { - background-color: transparent; + border-radius: 4px; + background-color: #e6e9e9; } } } diff --git a/src/components/PopoverDropdown/PopoverDropdown.tsx b/src/components/PopoverDropdown/PopoverDropdown.tsx index 7bb44723..f85ad505 100644 --- a/src/components/PopoverDropdown/PopoverDropdown.tsx +++ b/src/components/PopoverDropdown/PopoverDropdown.tsx @@ -1,8 +1,7 @@ import { useRef, useState } from 'react'; import { useOnClickOutside } from 'usehooks-ts'; import { LabelPairedEllipsisVerticalLgBoldIcon } from '@deriv/quill-icons'; -import { Button, Text, useDevice } from '@deriv-com/ui'; -import { TooltipMenuIcon } from '../TooltipMenuIcon'; +import { Button, Text, Tooltip, useDevice } from '@deriv-com/ui'; import './PopoverDropdown.scss'; type TItem = { @@ -28,13 +27,15 @@ const PopoverDropdown = ({ dropdownList, isBarred, onClick, tooltipMessage }: TP {isBarred ? ( ) : ( - setVisible(prevState => !prevState)} tooltipContent={tooltipMessage} + tooltipPosition='top' > - + )} {visible && (
diff --git a/src/components/ProfileContent/ProfileDailyLimit/ProfileDailyLimit.tsx b/src/components/ProfileContent/ProfileDailyLimit/ProfileDailyLimit.tsx index 25a7e6a0..5639575c 100644 --- a/src/components/ProfileContent/ProfileDailyLimit/ProfileDailyLimit.tsx +++ b/src/components/ProfileContent/ProfileDailyLimit/ProfileDailyLimit.tsx @@ -3,6 +3,7 @@ import { api } from '@/hooks'; import { useAdvertiserStats, useModalManager } from '@/hooks/custom-hooks'; import { Localize } from '@deriv-com/translations'; import { Button, Text, useDevice } from '@deriv-com/ui'; +import { FormatUtils } from '@deriv-com/utils'; import './ProfileDailyLimit.scss'; const ProfileDailyLimit = () => { @@ -21,8 +22,12 @@ const ProfileDailyLimit = () => { i18n_default_text='Want to increase your daily limits to <0>{{maxDailyBuy}} {{currency}} (buy) and <1>{{maxDailySell}} {{currency}} (sell)?' values={{ currency: activeAccount?.currency, - maxDailyBuy: advertiserStats?.daily_buy_limit, - maxDailySell: advertiserStats?.daily_sell_limit, + maxDailyBuy: FormatUtils.formatMoney( + Number(advertiserStats?.upgradable_daily_limits?.max_daily_buy) + ), + maxDailySell: FormatUtils.formatMoney( + Number(advertiserStats?.upgradable_daily_limits?.max_daily_sell) + ), }} /> diff --git a/src/components/TextField/HelperMessage.tsx b/src/components/TextField/HelperMessage.tsx deleted file mode 100644 index 51a6d7b1..00000000 --- a/src/components/TextField/HelperMessage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { ComponentProps, FC, InputHTMLAttributes, memo } from 'react'; -import { Text } from '@deriv-com/ui'; - -export type HelperMessageProps = { - inputValue?: InputHTMLAttributes['value']; - isError?: boolean; - maxLength?: InputHTMLAttributes['maxLength']; - message?: string; - messageVariant?: 'error' | 'general' | 'warning'; -}; - -const HelperMessage: FC = memo( - ({ inputValue, isError, maxLength, message, messageVariant = 'general' }) => { - const HelperMessageColors: Record['color']> = { - error: 'error', - general: 'less-prominent', - warning: 'warning', - }; - - return ( - <> - {message && ( -
- - {message} - -
- )} - {maxLength && ( -
- - {inputValue?.toString().length || 0} / {maxLength} - -
- )} - - ); - } -); - -HelperMessage.displayName = 'HelperMessage'; -export default HelperMessage; diff --git a/src/components/TextField/TextField.scss b/src/components/TextField/TextField.scss deleted file mode 100644 index b130873a..00000000 --- a/src/components/TextField/TextField.scss +++ /dev/null @@ -1,143 +0,0 @@ -.textfield { - min-width: 12rem; - width: 100%; - position: relative; - display: flex; - flex-direction: column; - gap: 0.2rem; - - &--error { - .textfield__box, - .textfield__box:hover { - border: 1px solid var(--status-light-danger, #ec3f3f); - } - - .textfield__box:has(.textfield__field:focus) { - border: 1px solid var(--brand-blue, #ec3f3f); - } - - .textfield__box:has(.textfield__field:valid) { - border: 1px solid var(--brand-blue, #ec3f3f); - } - - .textfield__label { - color: #ec3f3f; - } - - .textfield__field:focus ~ .textfield__label { - color: #ec3f3f; - } - } - - &--disabled { - & .textfield__box, - .textfield__box:hover { - border: 1px solid var(--system-light-5-active-background, #eaeced); - - & input { - color: var(--system-light-5-active-background, #999); - background: transparent; - } - } - & .textfield__box:has(.textfield__field:focus) { - border: 1px solid var(--system-light-5-active-background, #eaeced); - } - & .textfield__field { - background: inherit; - } - } - - &__box { - height: 4rem; - width: 100%; - border-radius: 0.4rem; - padding: 1rem 1.6rem; - border: 1px solid var(--system-light-5-active-background, #d6dadb); - display: inline-flex; - align-items: center; - transition: border-color 0.2s; - - &:hover { - border-color: var(--system-light-3-less-prominent-text, #999); - } - } - - &__box:has(&__field:focus) { - border: 1px solid var(--brand-blue, #85acb0); - } - - &__box:has(&__field:invalid) { - border: 1px solid var(--status-light-danger, #ec3f3f); - } - - &__field { - min-width: 0; - font-family: inherit; - outline: 0; - font-size: 1.4rem; - color: var(--system-light-2-general-text, #333); - transition: border-color 0.2s; - flex: 1; - } - - &__field::placeholder { - color: transparent; - } - - &__field:placeholder-shown ~ &__label { - font-size: 1.4rem; - cursor: text; - top: 2rem; - padding: 0; - } - - label, - &__field:focus ~ &__label { - position: absolute; - top: 0%; - transform: translateY(-50%); - display: block; - transition: 0.2s; - font-size: 1rem; - color: var(--system-light-3-less-prominent-text, #999); - background: var(--system-light-8-primary-background, #fff); - padding-inline: 0.4rem; - left: 1.6rem; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - } - - &__field:focus ~ &__label { - color: var(--brand-blue, #85acb0); - } - - &__field:invalid ~ &__label { - color: var(--status-light-danger, #ec3f3f); - } - - &__icon { - &-left { - margin-right: 0.8rem; - } - - &-right { - margin-left: 1.6rem; - } - } - - &__message-container { - height: 2rem; - padding: 0rem 0rem 0rem 1.6rem; - width: 100%; - - &--maxchar { - float: right; - } - - &--msg { - float: left; - text-align: left; - } - } -} diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx deleted file mode 100644 index 89c75860..00000000 --- a/src/components/TextField/TextField.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { ChangeEvent, ComponentProps, forwardRef, ReactNode, Ref, useState } from 'react'; -import clsx from 'clsx'; -import HelperMessage, { HelperMessageProps } from './HelperMessage'; -import './TextField.scss'; - -export interface TextFieldProps extends ComponentProps<'input'>, HelperMessageProps { - defaultValue?: string; - disabled?: boolean; - errorMessage?: string[] | string; - isInvalid?: boolean; - label?: string; - renderLeftIcon?: () => ReactNode; - renderRightIcon?: () => ReactNode; - shouldShowWarningMessage?: boolean; - showMessage?: boolean; -} - -const TextField = forwardRef( - ( - { - defaultValue = '', - disabled, - errorMessage, - isInvalid, - label, - maxLength, - message, - messageVariant = 'general', - name = 'textField', - onChange, - renderLeftIcon, - renderRightIcon, - shouldShowWarningMessage = false, - showMessage = false, - ...rest - }: TextFieldProps, - ref: Ref - ) => { - const [value, setValue] = useState(defaultValue); - - const handleChange = (e: ChangeEvent) => { - const newValue = e.target.value; - setValue(newValue); - onChange?.(e); - }; - - return ( -
-
- {typeof renderLeftIcon === 'function' && ( -
{renderLeftIcon()}
- )} - - {label && ( - - )} - {typeof renderRightIcon === 'function' && ( -
{renderRightIcon()}
- )} -
-
- {showMessage && !isInvalid && ( - - )} - {errorMessage && (isInvalid || (!isInvalid && shouldShowWarningMessage)) && ( - - )} -
-
- ); - } -); - -TextField.displayName = 'TextField'; -export default TextField; diff --git a/src/components/TextField/index.ts b/src/components/TextField/index.ts deleted file mode 100644 index 97d7c656..00000000 --- a/src/components/TextField/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Delete this component once @deriv-com/ui has it published -export { default as TextField } from './TextField'; diff --git a/src/components/TooltipMenuIcon/TooltipMenuIcon.scss b/src/components/TooltipMenuIcon/TooltipMenuIcon.scss deleted file mode 100644 index 94c03f33..00000000 --- a/src/components/TooltipMenuIcon/TooltipMenuIcon.scss +++ /dev/null @@ -1,5 +0,0 @@ -.tooltip-menu-icon { - &:hover { - background: #e6e9e9; - } -} diff --git a/src/components/TooltipMenuIcon/TooltipMenuIcon.tsx b/src/components/TooltipMenuIcon/TooltipMenuIcon.tsx deleted file mode 100644 index 951a9615..00000000 --- a/src/components/TooltipMenuIcon/TooltipMenuIcon.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ComponentProps, ElementType, PropsWithChildren } from 'react'; -import clsx from 'clsx'; -import { extendTheme, ThemeProvider, Tooltip, TooltipProps } from '@chakra-ui/react'; -import './TooltipMenuIcon.scss'; - -const theme = extendTheme({ - zIndices: { - tooltip: 1800, - }, -}); - -type AsElement = 'a' | 'button' | 'div'; -type TTooltipMenuIcon = ComponentProps & { - as: T; - disableHover?: boolean; - tooltipContent: string; - tooltipPosition?: TooltipProps['placement']; -}; - -// TODO replace this with deriv/ui -const TooltipMenuIcon = ({ - as, - children, - className, - disableHover = false, - tooltipContent, - tooltipPosition = 'top', - ...rest -}: PropsWithChildren>) => { - const Tag = as as ElementType; - - return ( - - - - {children} - - - - ); -}; - -export default TooltipMenuIcon; diff --git a/src/components/TooltipMenuIcon/__test__/TooltipMenuIcon.spec.tsx b/src/components/TooltipMenuIcon/__test__/TooltipMenuIcon.spec.tsx deleted file mode 100644 index 3c34da64..00000000 --- a/src/components/TooltipMenuIcon/__test__/TooltipMenuIcon.spec.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TooltipMenuIcon } from '..'; - -describe('TooltipMenuIcon Component', () => { - it('renders correctly with default props', () => { - render( - - Hover me - - ); - expect(screen.getByRole('button')).toHaveTextContent('Hover me'); - }); - - it('displays tooltip on hover', async () => { - render( - - Hover me - - ); - await userEvent.hover(screen.getByRole('button')); - expect(screen.getByText('Tooltip text')).toBeInTheDocument(); - }); - - it('accepts and applies custom tooltip position', async () => { - render( - - Hover me - - ); - await userEvent.hover(screen.getByRole('button')); - expect(screen.getByText('Tooltip text')).toBeInTheDocument(); - }); - - it("renders correctly with as='a'", () => { - render( - - Hover me - - ); - expect(screen.getByRole('link')).toHaveTextContent('Hover me'); - }); -}); diff --git a/src/components/TooltipMenuIcon/index.ts b/src/components/TooltipMenuIcon/index.ts deleted file mode 100644 index 6cb8e0f8..00000000 --- a/src/components/TooltipMenuIcon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TooltipMenuIcon } from './TooltipMenuIcon'; diff --git a/src/components/index.ts b/src/components/index.ts index a460022e..ed4d0a79 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,7 +14,6 @@ export * from './FileDropzone'; export * from './FloatingRate'; export * from './FormProgress'; export * from './FullPageMobileWrapper'; -export * from './Input'; export * from './LightDivider'; export * from './MobileTabs'; export * from './OnlineStatus'; @@ -33,7 +32,6 @@ export * from './Search'; export * from './StarRating'; export * from './Table'; export * from './TemporarilyBarredHint'; -export * from './TextField'; export * from './UserAvatar'; export * from './Verification'; export * from './Wizard'; diff --git a/src/constants/api-error-codes.ts b/src/constants/api-error-codes.ts index 35900e95..731ee6db 100644 --- a/src/constants/api-error-codes.ts +++ b/src/constants/api-error-codes.ts @@ -1,4 +1,5 @@ export const ERROR_CODES = { + ACCOUNT_DISABLED: 'AccountDisabled', AD_EXCEEDS_BALANCE: 'advertiser_balance', AD_EXCEEDS_DAILY_LIMIT: 'advertiser_daily_limit', ADVERT_INACTIVE: 'advert_inactive', @@ -17,4 +18,5 @@ export const ERROR_CODES = { ORDER_CREATE_FAIL_RATE_SLIPPAGE: 'OrderCreateFailRateSlippage', ORDER_EMAIL_VERIFICATION_REQUIRED: 'OrderEmailVerificationRequired', PERMISSION_DENIED: 'PermissionDenied', + TEMPORARY_BAR: 'TemporaryBar', } as const; diff --git a/src/constants/index.ts b/src/constants/index.ts index 01676826..7a662fd7 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,6 +4,7 @@ export * from './buy-sell'; export * from './chat-constants'; export * from './firebase-init-data'; export * from './languages'; +export * from './notification'; export * from './orders'; export * from './p2p-logo'; export * from './payment-methods'; diff --git a/src/constants/notification.ts b/src/constants/notification.ts new file mode 100644 index 00000000..c9d9f856 --- /dev/null +++ b/src/constants/notification.ts @@ -0,0 +1,40 @@ +import { TLocalize } from 'types'; +import { FormatUtils } from '@deriv-com/utils'; + +export const getNotification = (localize: TLocalize, messageKey: string, payload?: string) => { + const values = payload ? JSON.parse(payload) : {}; + const notification = { + actionText: '', + message: '', + title: '', + }; + + switch (messageKey) { + case 'p2p-order-complete': + notification.title = localize('Your order {{order_id}} is complete', values); + notification.message = localize( + '{{name}} has released your funds. Would you like to give your feedback?', + values + ); + notification.actionText = localize('Give feedback', values); + + break; + case 'p2p-limit-upgrade-available': + notification.title = localize('Enjoy higher daily limits', values); + notification.message = localize( + 'Would you like to increase your daily limits to {{new_buy_limit}} {{currency}} (buy) and {{new_sell_limit}} {{currency}} (sell)?', + { + currency: values.account_currency, + new_buy_limit: FormatUtils.formatMoney(values.new_buy_limit), + new_sell_limit: FormatUtils.formatMoney(values.new_sell_limit), + } + ); + notification.actionText = localize('Yes, increase my limits'); + + break; + default: + notification.message = messageKey; + } + + return notification; +}; diff --git a/src/hooks/api/account/useActiveAccount.ts b/src/hooks/api/account/useActiveAccount.ts index f72ace20..1e46b165 100644 --- a/src/hooks/api/account/useActiveAccount.ts +++ b/src/hooks/api/account/useActiveAccount.ts @@ -5,7 +5,7 @@ import { useAccountList, useAuthData } from '@deriv-com/api-hooks'; /** A custom hook that returns the account object for the current active account. */ const useActiveAccount = () => { const { data, ...rest } = useAccountList(); - const { activeLoginid } = useAuthData(); + const { activeLoginid, error } = useAuthData(); const { data: balanceData } = api.account.useBalance(); const activeAccount = useMemo( () => data?.find(account => account.loginid === activeLoginid), @@ -23,6 +23,7 @@ const useActiveAccount = () => { return { /** User's current active account. */ + authError: error, data: modifiedAccount, ...rest, }; diff --git a/src/hooks/api/index.ts b/src/hooks/api/index.ts index 46ae17ba..e296660f 100644 --- a/src/hooks/api/index.ts +++ b/src/hooks/api/index.ts @@ -4,6 +4,7 @@ export * from './advertiser'; export * from './chat'; export * from './counterparty'; export * from './exchange-rates'; +export * as notification from './notification'; export * from './order'; export * from './order-dispute'; export * from './order-review'; diff --git a/src/hooks/api/notification/__tests__/useNotificationList.spec.ts b/src/hooks/api/notification/__tests__/useNotificationList.spec.ts new file mode 100644 index 00000000..bf2a772e --- /dev/null +++ b/src/hooks/api/notification/__tests__/useNotificationList.spec.ts @@ -0,0 +1,39 @@ +import { renderHook } from '@testing-library/react'; +import useNotificationList from '../useNotificationList'; + +const mockNotificationListData = { + data: { + notifications_list: { + messages: [ + { + category: 'see', + id: 1, + message_key: 'p2p-limit-upgrade-available', + }, + { + category: 'see', + id: 2, + message_key: 'poi-verified', + }, + ], + }, + }, +}; + +jest.mock('@deriv-com/api-hooks', () => ({ + useSubscribe: jest.fn(() => mockNotificationListData), +})); + +describe('useNotificationList', () => { + it('should return the list of p2p-related notifications', () => { + const { result } = renderHook(() => useNotificationList()); + + expect(result.current.data).toEqual([ + { + category: 'see', + id: 1, + message_key: 'p2p-limit-upgrade-available', + }, + ]); + }); +}); diff --git a/src/hooks/api/notification/index.ts b/src/hooks/api/notification/index.ts new file mode 100644 index 00000000..0db7a4b1 --- /dev/null +++ b/src/hooks/api/notification/index.ts @@ -0,0 +1,2 @@ +export { default as useGetList } from './useNotificationList'; +export { default as useUpdate } from './useNotificationUpdate'; diff --git a/src/hooks/api/notification/useNotificationList.ts b/src/hooks/api/notification/useNotificationList.ts new file mode 100644 index 00000000..09a7aad1 --- /dev/null +++ b/src/hooks/api/notification/useNotificationList.ts @@ -0,0 +1,77 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useSubscribe } from '@deriv-com/api-hooks'; + +type TNotificationLinks = { + href: string; + rel: string; +}; +type TNotification = { + category: string; + id: number; + links: TNotificationLinks[]; + message_key: string; + payload: string; + read: boolean; + removed: boolean; +}; + +const handleData = (incomingMessages: TNotification[], prevMessages: TNotification[]) => { + if (!incomingMessages) return prevMessages; + + let notifications = prevMessages; + for (let updateIdx = 0; updateIdx < incomingMessages.length; updateIdx++) { + const update = incomingMessages[updateIdx]; + + const existingMessageIndex = notifications.findIndex((message: TNotification) => message.id === update.id); + const existingMessage = notifications[existingMessageIndex]; + + if (existingMessage) { + if (update.removed) + notifications = notifications.filter((message: TNotification) => message.id !== update.id); + else notifications[existingMessageIndex] = { ...existingMessage, ...update }; + } else notifications.unshift(update); + } + + notifications.sort((a: TNotification, b: TNotification) => b.id - a.id); + + return [...notifications]; +}; + +/** + * Hook that returns the list of notifications. + * + * @example const { data: notifications } = useNotificationList(); + */ +const useNotificationList = () => { + // @ts-expect-error Type undefined. This endpoint will be added to api-hooks. + const { data, ...rest } = useSubscribe('notifications_list'); + const [messages, setMessages] = useState([]); + + const modified_data = useMemo(() => { + if (!messages) return undefined; + + // TODO: Remove this filter once all the notifications are implemented + const notifications = messages.filter((notification: { message_key: string }) => + ['p2p-limit-upgrade-available'].includes(notification.message_key) + ); + + return notifications; + }, [messages]); + + useEffect(() => { + // @ts-expect-error Type undefined. + if (data?.notifications_list) { + setMessages(prevMessages => { + // @ts-expect-error Type undefined. + return handleData(data.notifications_list.messages, prevMessages); + }); + } + }, [data]); + + return { + data: modified_data || [], + ...rest, + }; +}; + +export default useNotificationList; diff --git a/src/hooks/api/notification/useNotificationUpdate.ts b/src/hooks/api/notification/useNotificationUpdate.ts new file mode 100644 index 00000000..8eeb47f5 --- /dev/null +++ b/src/hooks/api/notification/useNotificationUpdate.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@deriv-com/api-hooks'; + +/** + * Hook that updates the status of a notification. The notification can be removed or marked as read or unread. + * + * @example + * const { data, mutate } = useNotificationUpdate(); + * mutate({ notifications_update_status: 'read', ids: [notification_id]}); + * mutate({ notifications_update_status: 'unread', ids: [notification_id]}); + * mutate({ notifications_update_status: 'remove', ids: []}); + */ +const useNotificationUpdate = () => { + const { data, ...rest } = useMutation({ + // @ts-expect-error Type undefined. This endpoint will be added to api-hooks. + name: 'notifications_update_status', + }); + + return { + // @ts-expect-error Type undefined. + data: data?.notifications_update_status, + ...rest, + }; +}; + +export default useNotificationUpdate; diff --git a/src/hooks/custom-hooks/index.ts b/src/hooks/custom-hooks/index.ts index 64e06c1b..a8fe9d0b 100644 --- a/src/hooks/custom-hooks/index.ts +++ b/src/hooks/custom-hooks/index.ts @@ -6,6 +6,7 @@ export { default as useExtendedOrderDetails } from './useExtendedOrderDetails'; export { default as useFetchMore } from './useFetchMore'; export { default as useFloatingRate } from './useFloatingRate'; export { default as useFullScreen } from './useFullScreen'; +export { default as useGrowthbookGetFeatureValue } from './useGrowthbookGetFeatureValue'; export { default as useHandleRouteChange } from './useHandleRouteChange'; export { default as useIsAdvertiser } from './useIsAdvertiser'; export { default as useIsAdvertiserBarred } from './useIsAdvertiserBarred'; diff --git a/src/hooks/custom-hooks/useIsAdvertiserBarred.ts b/src/hooks/custom-hooks/useIsAdvertiserBarred.ts index 26009ad7..fa6b31b2 100644 --- a/src/hooks/custom-hooks/useIsAdvertiserBarred.ts +++ b/src/hooks/custom-hooks/useIsAdvertiserBarred.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import useInvalidateQuery from '../api/useInvalidateQuery'; import { api } from '..'; @@ -8,16 +8,14 @@ import { api } from '..'; */ const useIsAdvertiserBarred = (): boolean => { const { data = {} } = api.advertiser.useGetInfo(); - const [isAdvertiserBarred, setIsAdvertiserBarred] = useState(false); const invalidate = useInvalidateQuery(); - useEffect(() => { - if (isAdvertiserBarred !== !!data.blocked_until) { - invalidate('p2p_advertiser_adverts'); - setIsAdvertiserBarred(!!data.blocked_until); - } + const isAdvertiserBarred = useMemo(() => { + invalidate('p2p_advertiser_adverts'); + return !!data.blocked_until; + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAdvertiserBarred, data.blocked_until]); + }, [data.blocked_until]); return isAdvertiserBarred; }; diff --git a/src/pages/advertiser/screens/AdvertiserBlockOverlay/AdvertiserBlockOverlay.tsx b/src/pages/advertiser/screens/AdvertiserBlockOverlay/AdvertiserBlockOverlay.tsx index 298aa820..480e605d 100644 --- a/src/pages/advertiser/screens/AdvertiserBlockOverlay/AdvertiserBlockOverlay.tsx +++ b/src/pages/advertiser/screens/AdvertiserBlockOverlay/AdvertiserBlockOverlay.tsx @@ -32,7 +32,10 @@ const AdvertiserBlockOverlay = ({ size={isDesktop ? 'md' : 'lg'} weight='bold' > - +
{children} - setShowOverlay(false)} - onRequestClose={hideModal} - /> + {isModalOpenFor('BlockUnblockUserModal') && ( + setShowOverlay(false)} + onRequestClose={hideModal} + /> + )}
); } diff --git a/src/pages/buy-sell/components/SortDropdown/SortDropdown.scss b/src/pages/buy-sell/components/SortDropdown/SortDropdown.scss index 7de7291a..ab0dfd64 100644 --- a/src/pages/buy-sell/components/SortDropdown/SortDropdown.scss +++ b/src/pages/buy-sell/components/SortDropdown/SortDropdown.scss @@ -1,6 +1,6 @@ .sort-dropdown { .deriv-dropdown__items { - top: 4.4rem; + top: 3.6rem; width: 24rem; } diff --git a/src/pages/buy-sell/components/SortDropdown/SortDropdown.tsx b/src/pages/buy-sell/components/SortDropdown/SortDropdown.tsx index 28fc7e49..08662594 100644 --- a/src/pages/buy-sell/components/SortDropdown/SortDropdown.tsx +++ b/src/pages/buy-sell/components/SortDropdown/SortDropdown.tsx @@ -32,7 +32,7 @@ const SortDropdown = ({ list, onSelect, setIsFilterModalOpen, value }: TSortDrop return (
} + chevronIcon={} label={localize('Sort by')} list={list as unknown as MutableOption[]} name='Sort by' diff --git a/src/pages/buy-sell/screens/BuySell/BuySell.scss b/src/pages/buy-sell/screens/BuySell/BuySell.scss index a5c7410a..59fcd8b2 100644 --- a/src/pages/buy-sell/screens/BuySell/BuySell.scss +++ b/src/pages/buy-sell/screens/BuySell/BuySell.scss @@ -1,5 +1,10 @@ .buy-sell { height: 100%; + + &--not-verified { + overflow-y: auto; + height: calc(100% - 11rem); + } & .buy-sell-table { & .table { @include mobile-or-tablet-screen { diff --git a/src/pages/buy-sell/screens/BuySell/BuySell.tsx b/src/pages/buy-sell/screens/BuySell/BuySell.tsx index 23ae112b..35cb0b92 100644 --- a/src/pages/buy-sell/screens/BuySell/BuySell.tsx +++ b/src/pages/buy-sell/screens/BuySell/BuySell.tsx @@ -16,14 +16,14 @@ const BuySell = () => { if (poiPoaVerified === 'false') { return ( - <> +
history.replace({ pathname: BUY_SELL_URL, search: '' })} pageTitle={localize('Verification')} weight='bold' /> - +
); } diff --git a/src/pages/buy-sell/screens/BuySellTable/BuySellTableRenderer/BuySellTableRenderer.tsx b/src/pages/buy-sell/screens/BuySellTable/BuySellTableRenderer/BuySellTableRenderer.tsx index 976cfc05..a9a3964f 100644 --- a/src/pages/buy-sell/screens/BuySellTable/BuySellTableRenderer/BuySellTableRenderer.tsx +++ b/src/pages/buy-sell/screens/BuySellTable/BuySellTableRenderer/BuySellTableRenderer.tsx @@ -41,30 +41,32 @@ const BuySellTableRenderer = ({ if ((!data && !searchValue) || (data.length === 0 && !searchValue)) { return ( -
- history.push(MY_ADS_URL)} - size='lg' - textSize={isMobile ? 'md' : 'sm'} - > - - - } - description={ - - - - } - icon={} - title={ - - - - } - /> +
+
+ history.push(MY_ADS_URL)} + size='lg' + textSize={isMobile ? 'md' : 'sm'} + > + + + } + description={ + + + + } + icon={} + title={ + + + + } + /> +
); } diff --git a/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx b/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx index a731c6ad..5aadb98a 100644 --- a/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx +++ b/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx @@ -235,6 +235,7 @@ describe('', () => { }); it('should call history.replace if user clicks on Buy/Sell button and POA/POI is not verified', async () => { + mockUseIsAdvertiser = false; mockUseModalManager.isModalOpenFor.mockReturnValue(false); mockUsePoiPoaStatus.mockReturnValueOnce({ data: { isPoaVerified: false, isPoiVerified: false } }); diff --git a/src/pages/endpoint/screens/Endpoint/Endpoint.tsx b/src/pages/endpoint/screens/Endpoint/Endpoint.tsx index 2e1face3..e1c4ff42 100644 --- a/src/pages/endpoint/screens/Endpoint/Endpoint.tsx +++ b/src/pages/endpoint/screens/Endpoint/Endpoint.tsx @@ -1,4 +1,5 @@ import { Controller, useForm } from 'react-hook-form'; +import { getServerInfo } from '@/constants'; import { Button, Input, Text } from '@deriv-com/ui'; import { LocalStorageConstants, LocalStorageUtils } from '@deriv-com/utils'; import './Endpoint.scss'; @@ -86,7 +87,7 @@ const Endpoint = () => { LocalStorageUtils.setValue(LocalStorageConstants.configServerURL, ''); LocalStorageUtils.setValue(LocalStorageConstants.configAppId, ''); reset(); - window.location.reload(); + getServerInfo(); }} variant='outlined' > diff --git a/src/pages/endpoint/screens/Endpoint/__tests__/Endpoint.spec.tsx b/src/pages/endpoint/screens/Endpoint/__tests__/Endpoint.spec.tsx index 984e5b81..bc22acc7 100644 --- a/src/pages/endpoint/screens/Endpoint/__tests__/Endpoint.spec.tsx +++ b/src/pages/endpoint/screens/Endpoint/__tests__/Endpoint.spec.tsx @@ -2,6 +2,12 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Endpoint from '../Endpoint'; +const mockGetServerInfo = jest.fn(); + +jest.mock('@/constants', () => ({ + getServerInfo: jest.fn(() => mockGetServerInfo()), +})); + describe('', () => { it('should render the endpoint component', () => { render(); @@ -28,7 +34,7 @@ describe('', () => { expect(JSON.parse(localStorage.getItem('config.app_id') || '')).toBe('123'); }); - it('should reset the server_url and app_id when user clicks on the reset button', async () => { + it('should call getServerInfo and reset the inputs when user clicks on the reset button', async () => { render(); const serverUrlInput = screen.getByTestId('dt_endpoint_server_url_input'); @@ -41,5 +47,6 @@ describe('', () => { expect(JSON.parse(localStorage.getItem('config.server_url') || '')).toBe(''); expect(JSON.parse(localStorage.getItem('config.app_id') || '')).toBe(''); + expect(mockGetServerInfo).toHaveBeenCalled(); }); }); diff --git a/src/pages/my-ads/components/AdTypeSection/AdTypeSection.tsx b/src/pages/my-ads/components/AdTypeSection/AdTypeSection.tsx index 2c96626e..c2852a8f 100644 --- a/src/pages/my-ads/components/AdTypeSection/AdTypeSection.tsx +++ b/src/pages/my-ads/components/AdTypeSection/AdTypeSection.tsx @@ -112,6 +112,7 @@ const AdTypeSection = ({ currency, localCurrency, onCancel, rateType, ...props } } isDisabled={isEdit} label={localize('Total amount')} + maxLength={15} name='amount' rightPlaceholder={ @@ -142,6 +143,7 @@ const AdTypeSection = ({ currency, localCurrency, onCancel, rateType, ...props } ) : ( @@ -154,6 +156,7 @@ const AdTypeSection = ({ currency, localCurrency, onCancel, rateType, ...props }
@@ -164,6 +167,7 @@ const AdTypeSection = ({ currency, localCurrency, onCancel, rateType, ...props } /> diff --git a/src/pages/my-ads/components/AlertComponent/AlertComponent.scss b/src/pages/my-ads/components/AlertComponent/AlertComponent.scss index f5eb5c0a..e9455b8c 100644 --- a/src/pages/my-ads/components/AlertComponent/AlertComponent.scss +++ b/src/pages/my-ads/components/AlertComponent/AlertComponent.scss @@ -3,7 +3,7 @@ justify-content: center; position: absolute; align-items: center; - right: 3.6rem; + right: 4.6rem; height: 100%; span { @@ -12,7 +12,8 @@ @include mobile-or-tablet-screen { position: unset; margin-top: -0.4rem; - margin-left: 1rem; + margin-left: 2.5rem; + margin-right: 1.5rem; } & .tooltip-menu-icon:hover { diff --git a/src/pages/my-ads/components/AlertComponent/AlertComponent.tsx b/src/pages/my-ads/components/AlertComponent/AlertComponent.tsx index b0e296af..fdc13eaa 100644 --- a/src/pages/my-ads/components/AlertComponent/AlertComponent.tsx +++ b/src/pages/my-ads/components/AlertComponent/AlertComponent.tsx @@ -1,6 +1,6 @@ -import { TooltipMenuIcon } from '@/components/TooltipMenuIcon'; import { LegacyWarningIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; +import { Tooltip, useDevice } from '@deriv-com/ui'; import './AlertComponent.scss'; type TAlertComponentProps = { @@ -9,16 +9,12 @@ type TAlertComponentProps = { const AlertComponent = ({ onClick }: TAlertComponentProps) => { const { localize } = useTranslations(); + const { isDesktop } = useDevice(); return (
- + - +
); }; diff --git a/src/pages/my-ads/components/AlertComponent/__tests__/AlertComponent.spec.tsx b/src/pages/my-ads/components/AlertComponent/__tests__/AlertComponent.spec.tsx index 7ced1132..c9823418 100644 --- a/src/pages/my-ads/components/AlertComponent/__tests__/AlertComponent.spec.tsx +++ b/src/pages/my-ads/components/AlertComponent/__tests__/AlertComponent.spec.tsx @@ -5,6 +5,12 @@ import AlertComponent from '../AlertComponent'; const mockProps = { onClick: jest.fn(), }; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isDesktop: true }), +})); + describe('AlertComponent', () => { it('should render the component as expected', () => { render(); diff --git a/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.scss b/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.scss index 615047e3..6ecdb73e 100644 --- a/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.scss +++ b/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.scss @@ -23,7 +23,7 @@ } &__items { top: unset; - bottom: 5rem; + bottom: 7rem; max-height: 20rem; } } diff --git a/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.tsx b/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.tsx index e7df9525..b89d6342 100644 --- a/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.tsx +++ b/src/pages/my-ads/components/BuyPaymentMethodsList/BuyPaymentMethodsList.tsx @@ -14,9 +14,9 @@ const BuyPaymentMethodsList = ({ list, onSelectPaymentMethod }: TBuyPaymentMetho return (
} className='buy-payment-methods-list__dropdown' data-testid='dt_buy_payment_methods_list' - dropdownIcon={
} emptyResultMessage={localize('No results found')} icon={} isFullWidth diff --git a/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.scss b/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.scss index 17c3b3e3..130dc77c 100644 --- a/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.scss +++ b/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.scss @@ -1,14 +1,19 @@ .order-time-selection { margin: 1rem 0; - & .deriv-dropdown { &__items { + top: 4rem; box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.16); } - & .deriv-input__container { - width: 100%; - } + & .deriv-input { + &__container { + width: 100%; + } + &__helper-message { + display: none; + } + } & .deriv-dropdown__items--xs { margin-top: 0.5rem; max-height: 20rem; diff --git a/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.tsx b/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.tsx index 07274636..e5a9268f 100644 --- a/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.tsx +++ b/src/pages/my-ads/components/OrderTimeSelection/OrderTimeSelection.tsx @@ -1,13 +1,12 @@ -import { useEffect, useState } from 'react'; +import { MouseEvent, useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { TOrderExpiryOptions } from 'types'; import { OrderTimeTooltipModal } from '@/components/Modals'; -import { TooltipMenuIcon } from '@/components/TooltipMenuIcon'; import { getOrderTimeInfoMessage } from '@/constants'; import { formatTime, getOrderTimeCompletionList } from '@/utils'; -import { LabelPairedChevronDownMdRegularIcon, LabelPairedCircleInfoCaptionRegularIcon } from '@deriv/quill-icons'; +import { LabelPairedCircleInfoCaptionRegularIcon } from '@deriv/quill-icons'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { Dropdown, Text, useDevice } from '@deriv-com/ui'; +import { Dropdown, Text, Tooltip, useDevice } from '@deriv-com/ui'; import './OrderTimeSelection.scss'; const OrderTimeSelection = ({ orderExpiryOptions }: { orderExpiryOptions: TOrderExpiryOptions }) => { @@ -44,6 +43,10 @@ const OrderTimeSelection = ({ orderExpiryOptions }: { orderExpiryOptions: TOrder return options; }; + const handleDropdownClick = (event: MouseEvent) => { + event.preventDefault(); + }; + return (
@@ -51,9 +54,8 @@ const OrderTimeSelection = ({ orderExpiryOptions }: { orderExpiryOptions: TOrder - undefined : () => setIsModalOpen(true)} tooltipContent={getOrderTimeInfoMessage(localize)} type='button' @@ -63,24 +65,27 @@ const OrderTimeSelection = ({ orderExpiryOptions }: { orderExpiryOptions: TOrder height={24} width={24} /> - +
( - } - isFullWidth={isDesktop} - list={getOptions().sort((a, b) => a.value - b.value)} - name='order-completion-time' - onSelect={onChange} - shouldClearValue - value={value} - variant='comboBox' - /> +
+ a.value - b.value)} + name='order-completion-time' + onSelect={onChange} + shouldClearValue + value={value} + variant='comboBox' + /> +
)} /> diff --git a/src/pages/my-ads/components/OrderTimeSelection/__tests__/OrderTimeSelection.spec.tsx b/src/pages/my-ads/components/OrderTimeSelection/__tests__/OrderTimeSelection.spec.tsx index 35680858..685c418d 100644 --- a/src/pages/my-ads/components/OrderTimeSelection/__tests__/OrderTimeSelection.spec.tsx +++ b/src/pages/my-ads/components/OrderTimeSelection/__tests__/OrderTimeSelection.spec.tsx @@ -39,13 +39,13 @@ describe('OrderTimeSelection', () => { it('should not do anything on clicking info icon in desktop view', async () => { render(); await userEvent.click(screen.getByTestId('dt_order_info_icon')); - expect(screen.queryByRole('button', { name: 'Ok' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'OK' })).not.toBeInTheDocument(); }); it('should handle the modal open in mobile view', async () => { mockUseDevice.mockReturnValue({ isMobile: true }); render(); await userEvent.click(screen.getByTestId('dt_order_info_icon')); - const okButton = screen.getByRole('button', { name: 'Ok' }); + const okButton = screen.getByRole('button', { name: 'OK' }); expect(okButton).toBeInTheDocument(); await userEvent.click(okButton); expect(screen.queryByRole('button', { name: 'Ok' })).not.toBeInTheDocument(); diff --git a/src/pages/my-ads/screens/MyAds/MyAds.tsx b/src/pages/my-ads/screens/MyAds/MyAds.tsx index d7720417..e4aa57d0 100644 --- a/src/pages/my-ads/screens/MyAds/MyAds.tsx +++ b/src/pages/my-ads/screens/MyAds/MyAds.tsx @@ -1,13 +1,19 @@ import { TemporarilyBarredHint, Verification } from '@/components'; -import { useIsAdvertiserBarred, usePoiPoaStatus } from '@/hooks/custom-hooks'; +import { useIsAdvertiser, useIsAdvertiserBarred, usePoiPoaStatus } from '@/hooks/custom-hooks'; import { MyAdsTable } from './MyAdsTable'; const MyAds = () => { + const isAdvertiser = useIsAdvertiser(); const isAdvertiserBarred = useIsAdvertiserBarred(); const { data } = usePoiPoaStatus(); const { isPoaVerified, isPoiVerified } = data || {}; - if (!isPoaVerified || !isPoiVerified) return ; + if (!isAdvertiser && (!isPoaVerified || !isPoiVerified)) + return ( +
+ ; +
+ ); return (
diff --git a/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx b/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx index f7f3f7ff..d703fa57 100644 --- a/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx +++ b/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx @@ -9,6 +9,7 @@ jest.mock('@/components', () => ({ })); jest.mock('@/hooks/custom-hooks', () => ({ + useIsAdvertiser: jest.fn(() => false), useIsAdvertiserBarred: jest.fn().mockReturnValue(false), usePoiPoaStatus: jest.fn().mockReturnValue({ data: { diff --git a/src/pages/my-profile/screens/MyProfile/MyProfile.tsx b/src/pages/my-profile/screens/MyProfile/MyProfile.tsx index 649908d6..761e28c6 100644 --- a/src/pages/my-profile/screens/MyProfile/MyProfile.tsx +++ b/src/pages/my-profile/screens/MyProfile/MyProfile.tsx @@ -49,8 +49,12 @@ const MyProfile = () => { return ; } - if (!isPoiVerified || !isPoaVerified) { - return ; + if (!isAdvertiser && (!isPoiVerified || !isPoaVerified)) { + return ( +
+ +
+ ); } if (!isDesktop) { diff --git a/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx b/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx index 957ef026..4865140d 100644 --- a/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx +++ b/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx @@ -94,7 +94,8 @@ describe('MyProfile', () => { render(); expect(screen.getByTestId('dt_derivs-loader')).toBeInTheDocument(); }); - it('should render the verification component if user has not completed POI ', () => { + it('should render the verification component if a new user has not completed POI ', () => { + (mockUseIsAdvertiser as jest.Mock).mockReturnValueOnce(false); (mockUsePoiPoaStatus as jest.Mock).mockReturnValueOnce({ data: { isPoaVerified: true, isPoiVerified: false }, isLoading: false, @@ -103,7 +104,8 @@ describe('MyProfile', () => { render(); expect(screen.getByText('Verification')).toBeInTheDocument(); }); - it('should render the verification component if user has not completed POA', () => { + it('should render the verification component if a new user has not completed POA', () => { + (mockUseIsAdvertiser as jest.Mock).mockReturnValueOnce(false); (mockUsePoiPoaStatus as jest.Mock).mockReturnValueOnce({ data: { isPoaVerified: false, isPoiVerified: true }, isLoading: false, diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.scss b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.scss index e9e287f5..097d3f8f 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.scss +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.scss @@ -5,7 +5,7 @@ & .deriv-dropdown__items { width: 20rem; - margin-top: 0.8rem; + margin-top: 0.5rem; } & .deriv-input { @@ -14,6 +14,10 @@ &__container { width: 20rem; + & .deriv-input__helper-message { + display: none; + } + @include mobile-or-tablet-screen { width: 100%; } diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.tsx b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.tsx index adddf2e2..64cee15d 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.tsx +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesHeader/MyProfileCounterpartiesHeader.tsx @@ -34,7 +34,7 @@ const MyProfileCounterpartiesHeader = ({ /> {isDesktop ? ( } + chevronIcon={} label={localize('Filter by')} list={getCounterpartiesDropdownList(localize) as unknown as MutableOption[]} listHeight='sm' diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/MyProfileCounterpartiesTable.tsx b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/MyProfileCounterpartiesTable.tsx index 3fb98c44..085bddf2 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/MyProfileCounterpartiesTable.tsx +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/MyProfileCounterpartiesTable.tsx @@ -1,6 +1,10 @@ -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { useEffect } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { Table } from '@/components'; +import { ERROR_CODES } from '@/constants'; import { api } from '@/hooks'; +import { useIsAdvertiserBarred } from '@/hooks/custom-hooks'; +import { useErrorStore } from '@/stores'; import { DerivLightIcBlockedAdvertisersBarredIcon } from '@deriv/quill-icons'; import { Localize } from '@deriv-com/translations'; import { Loader, Text, useDevice } from '@deriv-com/ui'; @@ -18,21 +22,14 @@ type TMyProfileCounterpartiesTableRowRendererProps = { id?: string; is_blocked: boolean; name?: string; - setErrorMessage: Dispatch>; }; const MyProfileCounterpartiesTableRowRenderer = ({ id, is_blocked: isBlocked, name, - setErrorMessage, }: TMyProfileCounterpartiesTableRowRendererProps) => ( - + ); const MyProfileCounterpartiesTable = ({ @@ -50,8 +47,14 @@ const MyProfileCounterpartiesTable = ({ is_blocked: dropdownValue === 'blocked' ? 1 : 0, trade_partners: 1, }); - const [errorMessage, setErrorMessage] = useState(''); const { isDesktop } = useDevice(); + const { errorMessages, reset } = useErrorStore( + useShallow(state => ({ errorMessages: state.errorMessages, reset: state.reset })) + ); + const isAdvertiserBarred = useIsAdvertiserBarred(); + const errorMessage = errorMessages.find( + error => error.code === ERROR_CODES.TEMPORARY_BAR || error.code === ERROR_CODES.PERMISSION_DENIED + )?.message; useEffect(() => { if (data.length > 0) { @@ -62,6 +65,13 @@ const MyProfileCounterpartiesTable = ({ } }, [data, errorMessage, setShowHeader]); + useEffect(() => { + if (!isAdvertiserBarred && errorMessages.some(error => error.code === ERROR_CODES.TEMPORARY_BAR)) { + setShowHeader(true); + reset(); + } + }, [errorMessage, errorMessages, isAdvertiserBarred, reset, setShowHeader]); + if (isLoading) { return ; } @@ -93,7 +103,6 @@ const MyProfileCounterpartiesTable = ({ rowRender={(rowData: unknown) => ( )} tableClassname='my-profile-counterparties-table' diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/__tests__/MyProfileCounterpartiesTable.spec.tsx b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/__tests__/MyProfileCounterpartiesTable.spec.tsx index a199320c..6c59ecd3 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/__tests__/MyProfileCounterpartiesTable.spec.tsx +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTable/__tests__/MyProfileCounterpartiesTable.spec.tsx @@ -1,4 +1,5 @@ import { api } from '@/hooks'; +import { useIsAdvertiserBarred } from '@/hooks/custom-hooks'; import { render, screen, waitFor } from '@testing-library/react'; import MyProfileCounterpartiesTable from '../MyProfileCounterpartiesTable'; @@ -15,12 +16,18 @@ const mockApiValues = { loadMoreAdvertisers: jest.fn(), }; +const mockStore = { + errorMessages: [], + reset: jest.fn(), +}; + jest.mock('@deriv-com/ui', () => ({ ...jest.requireActual('@deriv-com/ui'), useDevice: jest.fn(() => ({ isMobile: false })), })); jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), api: { advertiser: { useGetList: jest.fn(() => mockApiValues), @@ -29,6 +36,14 @@ jest.mock('@/hooks', () => ({ useIsRtl: jest.fn(() => false), })); +jest.mock('@/hooks/custom-hooks', () => ({ + useIsAdvertiserBarred: jest.fn(() => false), +})); + +jest.mock('@/stores', () => ({ + useErrorStore: jest.fn(selector => (selector ? selector(mockStore) : mockStore)), +})); + const mockUseModalManager = { hideModal: jest.fn(), isModalOpenFor: jest.fn(), @@ -46,6 +61,8 @@ jest.mock('@/components/Modals/BlockUnblockUserModal', () => ({ })); const mockUseGetList = api.advertiser.useGetList as jest.Mock; +const mockUseIsAdvertiserBarred = useIsAdvertiserBarred as jest.Mock; + describe('MyProfileCounterpartiesTable', () => { it('should render the empty results when there is no data', () => { render(); @@ -82,4 +99,32 @@ describe('MyProfileCounterpartiesTable', () => { render(); expect(screen.getByText('There are no matching name.')).toBeInTheDocument(); }); + + it('should show error message when error code is TEMPORARY_BAR', () => { + // @ts-expect-error - mock values + mockStore.errorMessages = [{ code: 'TemporaryBar', message: 'Temporary Bar' }]; + mockUseIsAdvertiserBarred.mockReturnValue(true); + mockUseGetList.mockReturnValue({ + ...mockApiValues, + data: [{ id: 'id1', is_blocked: false, name: 'name1' }], + }); + + render(); + expect(screen.getByText('Temporary Bar')).toBeInTheDocument(); + }); + + it('should call reset and setShowHeader if isAdvertiserBarred is false and error code is', () => { + // @ts-expect-error - mock values + mockStore.errorMessages = [{ code: 'TemporaryBar', message: 'Temporary Bar' }]; + mockUseIsAdvertiserBarred.mockReturnValue(false); + mockUseGetList.mockReturnValue({ + ...mockApiValues, + data: [{ id: 'id1', is_blocked: false, name: 'name1' }], + }); + + render(); + + expect(mockProps.setShowHeader).toHaveBeenCalledWith(true); + expect(mockStore.reset).toHaveBeenCalled(); + }); }); diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/MyProfileCounterpartiesTableRow.tsx b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/MyProfileCounterpartiesTableRow.tsx index 161b80c9..962e008e 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/MyProfileCounterpartiesTableRow.tsx +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/MyProfileCounterpartiesTableRow.tsx @@ -1,4 +1,4 @@ -import { Dispatch, memo, SetStateAction } from 'react'; +import { memo } from 'react'; import clsx from 'clsx'; import { useHistory } from 'react-router-dom'; import { UserAvatar } from '@/components'; @@ -13,15 +13,9 @@ type TMyProfileCounterpartiesTableRowProps = { id: string; isBlocked: boolean; nickname: string; - setErrorMessage: Dispatch>; }; -const MyProfileCounterpartiesTableRow = ({ - id, - isBlocked, - nickname, - setErrorMessage, -}: TMyProfileCounterpartiesTableRowProps) => { +const MyProfileCounterpartiesTableRow = ({ id, isBlocked, nickname }: TMyProfileCounterpartiesTableRowProps) => { const { isDesktop } = useDevice(); const history = useHistory(); const { hideModal, isModalOpenFor, showModal } = useModalManager(); @@ -54,14 +48,15 @@ const MyProfileCounterpartiesTableRow = ({ {isBlocked ? localize('Unblock') : localize('Block')}
- + {isModalOpenFor('BlockUnblockUserModal') && ( + + )} ); }; diff --git a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/__tests__/MyProfileCounterpartiesTableRow.spec.tsx b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/__tests__/MyProfileCounterpartiesTableRow.spec.tsx index 56561134..76313e30 100644 --- a/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/__tests__/MyProfileCounterpartiesTableRow.spec.tsx +++ b/src/pages/my-profile/screens/MyProfileCounterparties/MyProfileCounterpartiesTableRow/__tests__/MyProfileCounterpartiesTableRow.spec.tsx @@ -34,6 +34,7 @@ const mockModalManager = { }; jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), api: { counterparty: { useBlock: () => ({ diff --git a/src/pages/orders/components/ChatFooter/ChatFooter.tsx b/src/pages/orders/components/ChatFooter/ChatFooter.tsx index 4950b3b6..6b8ef6b3 100644 --- a/src/pages/orders/components/ChatFooter/ChatFooter.tsx +++ b/src/pages/orders/components/ChatFooter/ChatFooter.tsx @@ -29,7 +29,7 @@ const ChatFooter = ({ isClosed, sendFile, sendMessage }: TChatFooterProps) => { return (
- +
); diff --git a/src/pages/orders/components/ChatFooter/__tests__/ChatFooter.spec.tsx b/src/pages/orders/components/ChatFooter/__tests__/ChatFooter.spec.tsx index 140db0e1..ee6d7a48 100644 --- a/src/pages/orders/components/ChatFooter/__tests__/ChatFooter.spec.tsx +++ b/src/pages/orders/components/ChatFooter/__tests__/ChatFooter.spec.tsx @@ -19,7 +19,7 @@ describe('ChatFooter', () => { }); it('should render the conversation closed message', () => { render(); - expect(screen.getByText('This conversation is closed')).toBeInTheDocument(); + expect(screen.getByText('This conversation is closed.')).toBeInTheDocument(); }); it('should expect value to be set on changing input', async () => { render(); diff --git a/src/routes/AppContent/index.scss b/src/routes/AppContent/index.scss index 3ed793dd..2b4454e1 100644 --- a/src/routes/AppContent/index.scss +++ b/src/routes/AppContent/index.scss @@ -63,3 +63,15 @@ .languages-modal__body { margin: 1.6rem; } + +// to fix the helper message position instead of individually changing for all the forms and fields +.deriv-input__helper-message { + position: unset; +} + +// to fix the issue with text capitalize in dropdown label +.deriv-dropdown { + label { + text-transform: none; + } +} diff --git a/src/routes/AppContent/index.tsx b/src/routes/AppContent/index.tsx index 098b73e2..82b34d66 100644 --- a/src/routes/AppContent/index.tsx +++ b/src/routes/AppContent/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { BlockedScenarios } from '@/components/BlockedScenarios'; import { BUY_SELL_URL, ERROR_CODES } from '@/constants'; -import { api, useIsP2PBlocked, useLiveChat } from '@/hooks'; +import { api, useIsP2PBlocked, useLiveChat, useOAuth } from '@/hooks'; import { GuideTooltip } from '@/pages/guide/components'; import { AdvertiserInfoStateProvider } from '@/providers/AdvertiserInfoStateProvider'; import { getCurrentRoute } from '@/utils'; @@ -17,10 +17,16 @@ const AppContent = () => { const history = useHistory(); const location = useLocation(); const { isDesktop } = useDevice(); - const { data: activeAccountData, isFetched, isLoading: isLoadingActiveAccount } = api.account.useActiveAccount(); + const { + authError, + data: activeAccountData, + isFetched, + isLoading: isLoadingActiveAccount, + } = api.account.useActiveAccount(); const { init: initLiveChat } = useLiveChat(); const { isP2PBlocked, status } = useIsP2PBlocked(); const { localize } = useTranslations(); + const { oAuthLogout } = useOAuth(); const routes = getRoutes(localize); const tabRoutesConfiguration = routes.filter( @@ -79,6 +85,10 @@ const AppContent = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [location]); + useEffect(() => { + if (authError?.code === ERROR_CODES.ACCOUNT_DISABLED) oAuthLogout(); + }, [authError, oAuthLogout]); + useEffect(() => { if (!isGtmTracking.current) { window.dataLayer.push({ event: 'allow_tracking' }); diff --git a/src/stores/__tests__/useErrorStore.spec.ts b/src/stores/__tests__/useErrorStore.spec.ts new file mode 100644 index 00000000..7084e4d4 --- /dev/null +++ b/src/stores/__tests__/useErrorStore.spec.ts @@ -0,0 +1,61 @@ +import { act } from 'react'; +import { renderHook } from '@testing-library/react'; +import useErrorStore from '../useErrorStore'; + +describe('useErrorStore', () => { + it('should update the store state correctly', () => { + const { result } = renderHook(() => useErrorStore()); + + expect(result.current.errorMessages).toEqual([]); + + act(() => { + result.current.setErrorMessages({ code: 'error-code', message: 'error-message' }); + }); + + expect(result.current.errorMessages).toEqual([{ code: 'error-code', message: 'error-message' }]); + + act(() => { + result.current.setErrorMessages(null); + }); + }); + + it('should not add the same error message twice', () => { + const { result } = renderHook(() => useErrorStore()); + + expect(result.current.errorMessages).toEqual([]); + + act(() => { + result.current.setErrorMessages({ code: 'error-code', message: 'error-message' }); + }); + + expect(result.current.errorMessages).toEqual([{ code: 'error-code', message: 'error-message' }]); + + act(() => { + result.current.setErrorMessages({ code: 'error-code', message: 'error-message' }); + }); + + expect(result.current.errorMessages).toEqual([{ code: 'error-code', message: 'error-message' }]); + + act(() => { + result.current.setErrorMessages(null); + }); + }); + + it('should reset the error messages', () => { + const { result } = renderHook(() => useErrorStore()); + + expect(result.current.errorMessages).toEqual([]); + + act(() => { + result.current.setErrorMessages({ code: 'error-code', message: 'error-message' }); + }); + + expect(result.current.errorMessages).toEqual([{ code: 'error-code', message: 'error-message' }]); + + act(() => { + result.current.reset(); + }); + + expect(result.current.errorMessages).toEqual([]); + }); +}); diff --git a/src/stores/index.ts b/src/stores/index.ts index 0d8fcad6..818cb5a4 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,2 +1,3 @@ export { default as useBuySellFiltersStore } from './useBuySellFiltersStore'; +export { default as useErrorStore } from './useErrorStore'; export { default as useTabsStore } from './useTabsStore'; diff --git a/src/stores/useErrorStore.ts b/src/stores/useErrorStore.ts new file mode 100644 index 00000000..8d3b3876 --- /dev/null +++ b/src/stores/useErrorStore.ts @@ -0,0 +1,39 @@ +import { create } from 'zustand'; + +type TError = { + code: string; + message: string; +}; + +type TErrorState = { + errorMessages: TError[]; +}; + +type TErrorAction = { + reset: () => void; + setErrorMessages: (errorMessages: TError | null) => void; +}; + +const useErrorStore = create()(set => ({ + errorMessages: [], + reset: () => set({ errorMessages: [] }), + setErrorMessages: errorMessages => + set(state => { + if (!errorMessages) { + return { ...state, errorMessages: [] }; + } + + const isErrorMessageIncluded = state.errorMessages.some( + error => error.code === errorMessages.code && error.message === errorMessages.message + ); + + return { + ...state, + errorMessages: isErrorMessageIncluded + ? state.errorMessages + : [...state.errorMessages, { code: errorMessages.code, message: errorMessages.message }], + }; + }), +})); + +export default useErrorStore; diff --git a/src/utils/ad-utils.ts b/src/utils/ad-utils.ts index be6ea2e9..6d98ac54 100644 --- a/src/utils/ad-utils.ts +++ b/src/utils/ad-utils.ts @@ -108,7 +108,7 @@ export const getValidationRules = ( case 'min-order': return { validation_1: value => requiredValidation(value, localize('Min limit')), - validation_2: value => !isNaN(Number(value)) || localize('Only numbers are allowed'), + validation_2: value => !isNaN(Number(value)) || localize('Only numbers are allowed.'), validation_3: value => decimalPointValidation(value), validation_4: value => { const amount = getValues('amount'); @@ -128,7 +128,7 @@ export const getValidationRules = ( case 'max-order': return { validation_1: value => requiredValidation(value, localize('Max limit')), - validation_2: value => !isNaN(Number(value)) || 'Only numbers are allowed', + validation_2: value => !isNaN(Number(value)) || 'Only numbers are allowed.', validation_3: value => decimalPointValidation(value), validation_4: value => { const amount = getValues('amount');