diff --git a/cypress/e2e/wallet-dropdown.test.ts b/cypress/e2e/wallet-dropdown.test.ts index 46246eab48..2378d3889d 100644 --- a/cypress/e2e/wallet-dropdown.test.ts +++ b/cypress/e2e/wallet-dropdown.test.ts @@ -144,4 +144,32 @@ describe('Wallet Dropdown', () => { cy.get(getTestSelector('wallet-settings')).should('not.be.visible') }) }) + + describe('local currency', () => { + it('loads local currency from the query param', () => { + cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.contains('USD') + + cy.visit('/?cur=AUD', { featureFlags: [FeatureFlag.currencyConversion] }) + cy.contains('AUD') + }) + + it('loads local currency from menu', () => { + cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.contains('USD') + + cy.get(getTestSelector('local-currency-settings-button')).click() + cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true }) + cy.location('hash').should('match', /\?cur=AUD$/) + cy.contains('AUD') + + cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true }) + cy.location('hash').should('match', /\?cur=USD$/) + cy.contains('USD') + }) + }) }) diff --git a/src/components/AccountDrawer/DefaultMenu.tsx b/src/components/AccountDrawer/DefaultMenu.tsx index ed7615f693..f3e2605387 100644 --- a/src/components/AccountDrawer/DefaultMenu.tsx +++ b/src/components/AccountDrawer/DefaultMenu.tsx @@ -1,11 +1,12 @@ import { useWeb3React } from '@web3-react/core' import Column from 'components/Column' import WalletModal from 'components/WalletModal' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import styled from 'styled-components' import AuthenticatedHeader from './AuthenticatedHeader' import LanguageMenu from './LanguageMenu' +import LocalCurrencyMenu from './LocalCurrencyMenu' import SettingsMenu from './SettingsMenu' const DefaultMenuWrap = styled(Column)` @@ -17,6 +18,7 @@ enum MenuState { DEFAULT, SETTINGS, LANGUAGE_SETTINGS, + LOCAL_CURRENCY_SETTINGS, } function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { @@ -27,6 +29,7 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { const openSettings = useCallback(() => setMenu(MenuState.SETTINGS), []) const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), []) const openLanguageSettings = useCallback(() => setMenu(MenuState.LANGUAGE_SETTINGS), []) + const openLocalCurrencySettings = useCallback(() => setMenu(MenuState.LOCAL_CURRENCY_SETTINGS), []) useEffect(() => { if (!drawerOpen && menu !== MenuState.DEFAULT) { @@ -39,20 +42,30 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { return }, [drawerOpen, menu, closeSettings]) - return ( - - {menu === MenuState.DEFAULT && - (isAuthenticated ? ( + const SubMenu = useMemo(() => { + switch (menu) { + case MenuState.DEFAULT: + return isAuthenticated ? ( ) : ( - ))} - {menu === MenuState.SETTINGS && ( - - )} - {menu === MenuState.LANGUAGE_SETTINGS && } - - ) + ) + case MenuState.SETTINGS: + return ( + + ) + case MenuState.LANGUAGE_SETTINGS: + return + case MenuState.LOCAL_CURRENCY_SETTINGS: + return + } + }, [account, closeSettings, isAuthenticated, menu, openLanguageSettings, openLocalCurrencySettings, openSettings]) + + return {SubMenu} } export default DefaultMenu diff --git a/src/components/AccountDrawer/LanguageMenu.tsx b/src/components/AccountDrawer/LanguageMenu.tsx index f7aa133467..8723dcccb3 100644 --- a/src/components/AccountDrawer/LanguageMenu.tsx +++ b/src/components/AccountDrawer/LanguageMenu.tsx @@ -2,36 +2,21 @@ import { Trans } from '@lingui/macro' import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales' import { useActiveLocale } from 'hooks/useActiveLocale' import { useLocationLinkProps } from 'hooks/useLocationLinkProps' -import { Check } from 'react-feather' -import { Link } from 'react-router-dom' -import styled, { useTheme } from 'styled-components' -import { ClickableStyle, ThemedText } from 'theme' +import { MenuColumn, MenuItem } from './shared' import { SlideOutMenu } from './SlideOutMenu' -const InternalLinkMenuItem = styled(Link)` - ${ClickableStyle} - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - padding: 12px 0; - justify-content: space-between; - text-decoration: none; - color: ${({ theme }) => theme.neutral1}; -` - function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isActive: boolean }) { const { to, onClick } = useLocationLinkProps(locale) - const theme = useTheme() - - if (!to) return null return ( - - {LOCALE_LABEL[locale]} - {isActive && } - + ) } @@ -50,7 +35,9 @@ export function LanguageMenuItems() { export default function LanguageMenu({ onClose }: { onClose: () => void }) { return ( Language} onClose={onClose}> - + + + ) } diff --git a/src/components/AccountDrawer/LocalCurrencyMenu.tsx b/src/components/AccountDrawer/LocalCurrencyMenu.tsx new file mode 100644 index 0000000000..1d5de9620d --- /dev/null +++ b/src/components/AccountDrawer/LocalCurrencyMenu.tsx @@ -0,0 +1,61 @@ +import { Trans } from '@lingui/macro' +import { getLocalCurrencyIcon, SUPPORTED_LOCAL_CURRENCIES, SupportedLocalCurrency } from 'constants/localCurrencies' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { useLocalCurrencyLinkProps } from 'hooks/useLocalCurrencyLinkProps' +import { useMemo } from 'react' +import styled from 'styled-components' + +import { MenuColumn, MenuItem } from './shared' +import { SlideOutMenu } from './SlideOutMenu' + +const StyledLocalCurrencyIcon = styled.div` + width: 20px; + height: 20px; + border-radius: 100%; + overflow: hidden; +` + +function LocalCurrencyMenuItem({ + localCurrency, + isActive, +}: { + localCurrency: SupportedLocalCurrency + isActive: boolean +}) { + const { to, onClick } = useLocalCurrencyLinkProps(localCurrency) + + const LocalCurrencyIcon = useMemo(() => { + return {getLocalCurrencyIcon(localCurrency)} + }, [localCurrency]) + + if (!to) return null + + return ( + + ) +} + +export default function LocalCurrencyMenu({ onClose }: { onClose: () => void }) { + const activeLocalCurrency = useActiveLocalCurrency() + + return ( + Currency} onClose={onClose}> + + {SUPPORTED_LOCAL_CURRENCIES.map((localCurrency) => ( + + ))} + + + ) +} diff --git a/src/components/AccountDrawer/SettingsMenu.tsx b/src/components/AccountDrawer/SettingsMenu.tsx index c706705e22..a804c958c0 100644 --- a/src/components/AccountDrawer/SettingsMenu.tsx +++ b/src/components/AccountDrawer/SettingsMenu.tsx @@ -3,6 +3,7 @@ import Column from 'components/Column' import Row from 'components/Row' import { LOCALE_LABEL } from 'constants/locales' import { useCurrencyConversionFlagEnabled } from 'featureFlags/flags/currencyConversion' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' import { useActiveLocale } from 'hooks/useActiveLocale' import { ReactNode } from 'react' import { ChevronRight } from 'react-feather' @@ -27,15 +28,16 @@ const SectionTitle = styled(ThemedText.SubHeader)` padding-bottom: 24px; ` -const ToggleWrapper = styled.div` +const ToggleWrapper = styled.div<{ currencyConversionEnabled?: boolean }>` display: flex; flex-direction: column; gap: 16px; - margin-bottom: 24px; + margin-bottom: ${({ currencyConversionEnabled }) => (currencyConversionEnabled ? '10px' : '24px')}; ` const SettingsButtonWrapper = styled(Row)` ${ClickableStyle} + padding: 16px 0px; ` const StyledChevron = styled(ChevronRight)` @@ -60,7 +62,7 @@ const SettingsButton = ({ {title} - {currentState} + {currentState} @@ -69,12 +71,15 @@ const SettingsButton = ({ export default function SettingsMenu({ onClose, openLanguageSettings, + openLocalCurrencySettings, }: { onClose: () => void openLanguageSettings: () => void + openLocalCurrencySettings: () => void }) { const currencyConversionEnabled = useCurrencyConversionFlagEnabled() const activeLocale = useActiveLocale() + const activeLocalCurrency = useActiveLocalCurrency() return ( Settings} onClose={onClose}> @@ -83,7 +88,7 @@ export default function SettingsMenu({ Preferences - + @@ -99,12 +104,20 @@ export default function SettingsMenu({ )} {currencyConversionEnabled && ( - Language} - currentState={LOCALE_LABEL[activeLocale]} - onClick={openLanguageSettings} - testId="language-settings-button" - /> + + Language} + currentState={LOCALE_LABEL[activeLocale]} + onClick={openLanguageSettings} + testId="language-settings-button" + /> + Currency} + currentState={activeLocalCurrency} + onClick={openLocalCurrencySettings} + testId="local-currency-settings-button" + /> + )} diff --git a/src/components/AccountDrawer/shared.tsx b/src/components/AccountDrawer/shared.tsx new file mode 100644 index 0000000000..5ec69265f0 --- /dev/null +++ b/src/components/AccountDrawer/shared.tsx @@ -0,0 +1,57 @@ +import Column from 'components/Column' +import Row from 'components/Row' +import { ReactNode } from 'react' +import { Check } from 'react-feather' +import type { To } from 'react-router-dom' +import { Link } from 'react-router-dom' +import styled, { useTheme } from 'styled-components' +import { BREAKPOINTS, ClickableStyle, ThemedText } from 'theme' + +const InternalLinkMenuItem = styled(Link)` + ${ClickableStyle} + + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + padding: 12px 0; + justify-content: space-between; + text-decoration: none; + color: ${({ theme }) => theme.neutral1}; +` + +export const MenuColumn = styled(Column)` + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + padding-bottom: 14px; + } +` + +export function MenuItem({ + label, + logo, + to, + onClick, + isActive, + testId, +}: { + label: ReactNode + logo?: ReactNode + to?: To + onClick?: () => void + isActive: boolean + testId?: string +}) { + const theme = useTheme() + + if (!to) return null + + return ( + + + {logo && logo} + {label} + + {isActive && } + + ) +} diff --git a/src/constants/localCurrencies.tsx b/src/constants/localCurrencies.tsx new file mode 100644 index 0000000000..63f2215a3c --- /dev/null +++ b/src/constants/localCurrencies.tsx @@ -0,0 +1,90 @@ +import { ReactNode } from 'react' + +import { + AUD_ICON, + BRL_ICON, + CAD_ICON, + EUR_ICON, + GBP_ICON, + HKD_ICON, + IDR_ICON, + INR_ICON, + JPY_ICON, + NGN_ICON, + PKR_ICON, + RUB_ICON, + SGD_ICON, + THB_ICON, + TRY_ICON, + UAH_ICON, + USD_ICON, + VND_ICON, +} from './localCurrencyIcons' + +export const SUPPORTED_LOCAL_CURRENCIES = [ + 'USD', + 'AUD', + 'BRL', + 'CAD', + 'EUR', + 'GBP', + 'HKD', + 'IDR', + 'INR', + 'JPY', + 'NGN', + 'PKR', + 'RUB', + 'SGD', + 'THB', + 'TRY', + 'UAH', + 'VND', +] + +export type SupportedLocalCurrency = (typeof SUPPORTED_LOCAL_CURRENCIES)[number] + +export const DEFAULT_LOCAL_CURRENCY: SupportedLocalCurrency = 'USD' + +export function getLocalCurrencyIcon(localCurrency: SupportedLocalCurrency, size = 20): ReactNode { + switch (localCurrency) { + case 'USD': + return + case 'EUR': + return + case 'RUB': + return + case 'INR': + return + case 'GBP': + return + case 'JPY': + return + case 'VND': + return + case 'SGD': + return + case 'BRL': + return + case 'HKD': + return + case 'CAD': + return + case 'IDR': + return + case 'TRY': + return + case 'NGN': + return + case 'AUD': + return + case 'PKR': + return + case 'UAH': + return + case 'THB': + return + default: + return null + } +} diff --git a/src/constants/localCurrencyIcons.tsx b/src/constants/localCurrencyIcons.tsx new file mode 100644 index 0000000000..d8efefef02 --- /dev/null +++ b/src/constants/localCurrencyIcons.tsx @@ -0,0 +1,721 @@ +type SVGProps = React.SVGProps + +export const USD_ICON = (props: SVGProps) => ( + + + + + + +) + +export const EUR_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + +) + +export const RUB_ICON = (props: SVGProps) => ( + + + + + + + + + + + + +) + +export const INR_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + +) + +export const GBP_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + +) + +export const JPY_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const VND_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const SGD_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + +) + +export const BRL_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export const HKD_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export const CAD_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const IDR_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const TRY_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + +) + +export const NGN_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const AUD_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + +) + +export const PKR_ICON = (props: SVGProps) => ( + + + + + + + + + + + + + +) + +export const UAH_ICON = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const THB_ICON = (props: SVGProps) => ( + + + + + + + + + + + + +) diff --git a/src/hooks/useActiveLocalCurrency.ts b/src/hooks/useActiveLocalCurrency.ts new file mode 100644 index 0000000000..b5ae8e9e4f --- /dev/null +++ b/src/hooks/useActiveLocalCurrency.ts @@ -0,0 +1,29 @@ +import { DEFAULT_LOCAL_CURRENCY, SUPPORTED_LOCAL_CURRENCIES, SupportedLocalCurrency } from 'constants/localCurrencies' +import { atomWithStorage, useAtomValue } from 'jotai/utils' +import { useMemo } from 'react' + +import useParsedQueryString from './useParsedQueryString' + +export const activeLocalCurrencyAtom = atomWithStorage( + 'activeLocalCurrency', + DEFAULT_LOCAL_CURRENCY +) + +function useUrlLocalCurrency() { + const parsed = useParsedQueryString() + const parsedLocalCurrency = parsed.cur + + if (typeof parsedLocalCurrency !== 'string') return undefined + + const lowerCaseSupportedLocalCurrency = parsedLocalCurrency.toLowerCase() + return SUPPORTED_LOCAL_CURRENCIES.find( + (localCurrency) => localCurrency.toLowerCase() === lowerCaseSupportedLocalCurrency + ) +} + +export function useActiveLocalCurrency(): SupportedLocalCurrency { + const activeLocalCurrency = useAtomValue(activeLocalCurrencyAtom) + const urlLocalCurrency = useUrlLocalCurrency() + + return useMemo(() => urlLocalCurrency ?? activeLocalCurrency, [activeLocalCurrency, urlLocalCurrency]) +} diff --git a/src/hooks/useLocalCurrencyLinkProps.ts b/src/hooks/useLocalCurrencyLinkProps.ts new file mode 100644 index 0000000000..e1467cf3bf --- /dev/null +++ b/src/hooks/useLocalCurrencyLinkProps.ts @@ -0,0 +1,40 @@ +import { sendAnalyticsEvent } from 'analytics' +import { SupportedLocalCurrency } from 'constants/localCurrencies' +import useParsedQueryString from 'hooks/useParsedQueryString' +import { useAtom } from 'jotai' +import { stringify } from 'qs' +import { useMemo } from 'react' +import type { To } from 'react-router-dom' +import { useLocation } from 'react-router-dom' + +import { activeLocalCurrencyAtom, useActiveLocalCurrency } from './useActiveLocalCurrency' + +export function useLocalCurrencyLinkProps(localCurrency?: SupportedLocalCurrency): { + to?: To + onClick?: () => void +} { + const location = useLocation() + const qs = useParsedQueryString() + const activeLocalCurrency = useActiveLocalCurrency() + const [, updateActiveLocalCurrency] = useAtom(activeLocalCurrencyAtom) + + return useMemo( + () => + !localCurrency + ? {} + : { + to: { + ...location, + search: stringify({ ...qs, cur: localCurrency }), + }, + onClick: () => { + updateActiveLocalCurrency(localCurrency) + sendAnalyticsEvent('Local Currency Selected', { + previous_local_currency: activeLocalCurrency, + new_local_currency: localCurrency, + }) + }, + }, + [localCurrency, location, qs, updateActiveLocalCurrency, activeLocalCurrency] + ) +} diff --git a/src/theme/components/text.tsx b/src/theme/components/text.tsx index cb511c241e..f80a1145b0 100644 --- a/src/theme/components/text.tsx +++ b/src/theme/components/text.tsx @@ -40,9 +40,6 @@ export const ThemedText = { Hero(props: TextProps) { return }, - LabelMedium(props: TextProps) { - return - }, LabelSmall(props: TextProps) { return },