diff --git a/apps/trading/client-pages/home/home.tsx b/apps/trading/client-pages/home/home.tsx index 7d0b48cf2..c4974bc99 100644 --- a/apps/trading/client-pages/home/home.tsx +++ b/apps/trading/client-pages/home/home.tsx @@ -1,20 +1,16 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { marketsWithDataProvider } from '@vegaprotocol/markets'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Links, Routes } from '../../pages/client-router'; import { useGlobalStore } from '../../stores'; +import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets'; +// The home pages only purpose is to redirect to the users last market, +// the top traded if they are new, or fall back to the list of markets. +// Thats why we just render a loader here export const Home = () => { const navigate = useNavigate(); - // The default market selected in the platform behind the overlay - // should be the oldest market that is currently trading in continuous mode(i.e. not in auction). - const { data, error, loading } = useDataProvider({ - dataProvider: marketsWithDataProvider, - variables: undefined, - }); - const update = useGlobalStore((store) => store.update); + const { data } = useTopTradedMarkets(); const marketId = useGlobalStore((store) => store.marketId); useEffect(() => { @@ -32,12 +28,11 @@ export const Home = () => { navigate(Links[Routes.MARKETS]()); } } - }, [marketId, data, navigate, update]); + }, [marketId, data, navigate]); return ( - - {/* Render a loading and error state but we will redirect if markets are found */} - {null} - + + + ); }; diff --git a/apps/trading/components/constants.ts b/apps/trading/components/constants.ts index 4e9781d22..b157c3b5f 100644 --- a/apps/trading/components/constants.ts +++ b/apps/trading/components/constants.ts @@ -1,2 +1 @@ export const THROTTLE_UPDATE_TIME = 500; -export const ONBOARDING_VIEWED_KEY = 'vega_onboarding_viewed'; diff --git a/apps/trading/components/telemetry/index.ts b/apps/trading/components/telemetry/index.ts new file mode 100644 index 000000000..a790051f1 --- /dev/null +++ b/apps/trading/components/telemetry/index.ts @@ -0,0 +1 @@ +export { Telemetry } from './telemetry'; diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx b/apps/trading/components/telemetry/telemetry-approval.spec.tsx similarity index 100% rename from apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx rename to apps/trading/components/telemetry/telemetry-approval.spec.tsx diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.tsx b/apps/trading/components/telemetry/telemetry-approval.tsx similarity index 100% rename from apps/trading/components/welcome-dialog/telemetry-approval.tsx rename to apps/trading/components/telemetry/telemetry-approval.tsx diff --git a/apps/trading/components/telemetry/telemetry.tsx b/apps/trading/components/telemetry/telemetry.tsx new file mode 100644 index 000000000..e40a5e2d3 --- /dev/null +++ b/apps/trading/components/telemetry/telemetry.tsx @@ -0,0 +1,69 @@ +import type { Toast } from '@vegaprotocol/ui-toolkit'; +import { Intent, useToasts } from '@vegaprotocol/ui-toolkit'; +import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval'; +import { useCallback, useEffect } from 'react'; +import { TelemetryApproval } from './telemetry-approval'; +import { t } from '@vegaprotocol/i18n'; +import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step'; + +const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id'; + +export const Telemetry = () => { + const onboardingDissmissed = useOnboardingStore((store) => store.dismissed); + const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] = + useTelemetryApproval(); + + const [setToast, hasToast, removeToast] = useToasts((store) => [ + store.setToast, + store.hasToast, + store.remove, + ]); + + const onApprovalClose = useCallback(() => { + closeTelemetry(); + removeToast(TELEMETRY_APPROVAL_TOAST_ID); + }, [closeTelemetry, removeToast]); + + const setTelemetryApprovalAndClose = useCallback( + (value: string) => { + setTelemetryValue(value); + onApprovalClose(); + }, + [onApprovalClose, setTelemetryValue] + ); + + useEffect(() => { + if (isTelemetryNeeded && onboardingDissmissed) { + const toast: Toast = { + id: TELEMETRY_APPROVAL_TOAST_ID, + intent: Intent.Primary, + content: ( + <> +

+ {t('Improve vega console')} +

+ + + ), + onClose: onApprovalClose, + }; + if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) { + setToast(toast); + } + return; + } + }, [ + telemetryValue, + isTelemetryNeeded, + onboardingDissmissed, + setToast, + hasToast, + onApprovalClose, + setTelemetryApprovalAndClose, + ]); + + return null; +}; diff --git a/apps/trading/components/welcome-dialog/get-started.spec.tsx b/apps/trading/components/welcome-dialog/get-started.spec.tsx index 3f5c12462..e0f805e33 100644 --- a/apps/trading/components/welcome-dialog/get-started.spec.tsx +++ b/apps/trading/components/welcome-dialog/get-started.spec.tsx @@ -2,7 +2,8 @@ import { MemoryRouter } from 'react-router-dom'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { GetStarted } from './get-started'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { useOnboardingStore } from './use-get-onboarding-step'; let mockStep = 1; jest.mock('./use-get-onboarding-step', () => ({ @@ -44,13 +45,15 @@ describe('GetStarted', () => { globalThis.window.vega = undefined as unknown as Vega; }); - it('renders nothing if connected', () => { + it('renders nothing if dismissed', () => { + useOnboardingStore.setState({ dismissed: true }); mockStep = 0; const { container } = renderComponent({ pubKey: 'my-pubkey' }); expect(container).toBeEmptyDOMElement(); }); it('steps should be ticked', () => { + useOnboardingStore.setState({ dismissed: false }); const navigatorGetter: jest.SpyInstance = jest.spyOn( window.navigator, 'userAgent', @@ -87,6 +90,8 @@ describe('GetStarted', () => { screen.getByRole('button', { name: 'Ready to trade' }) ).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Ready to trade' })); + mockStep = 5; rerender( diff --git a/apps/trading/components/welcome-dialog/get-started.tsx b/apps/trading/components/welcome-dialog/get-started.tsx index 086fdc58e..c496a5f55 100644 --- a/apps/trading/components/welcome-dialog/get-started.tsx +++ b/apps/trading/components/welcome-dialog/get-started.tsx @@ -9,17 +9,15 @@ import { } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; -import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useNavigate } from 'react-router-dom'; import { OnboardingStep, useGetOnboardingStep, + useOnboardingStore, } from './use-get-onboarding-step'; import { Links, Routes } from '../../pages/client-router'; import { useGlobalStore } from '../../stores'; import { useSidebar, ViewType } from '../sidebar'; -import * as constants from '../constants'; -import { useOnboardingStore } from './welcome-dialog'; interface Props { lead?: string; @@ -27,11 +25,8 @@ interface Props { const GetStartedButton = ({ step }: { step: OnboardingStep }) => { const navigate = useNavigate(); - const [, setOnboardingViewed] = useLocalStorage( - constants.ONBOARDING_VIEWED_KEY - ); - const dismiss = useOnboardingStore((store) => store.dismiss); + const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen); const marketId = useGlobalStore((store) => store.marketId); const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME](); const openVegaWalletDialog = useVegaWalletDialogStore( @@ -49,14 +44,14 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { onClickHandle = () => { navigate(link); setView({ type: ViewType.Deposit }); - dismiss(); + setDialogOpen(false); }; - } else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) { + } else if (step >= OnboardingStep.ONBOARDING_ORDER_STEP) { buttonText = t('Ready to trade'); onClickHandle = () => { navigate(link); setView({ type: ViewType.Order }); - setOnboardingViewed('true'); + dismiss(); }; } @@ -75,17 +70,12 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { export const GetStarted = ({ lead }: Props) => { const { pubKey } = useVegaWallet(); const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment(); - const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz'; - const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY); - const currentStep = useGetOnboardingStep(); const openVegaWalletDialog = useVegaWalletDialogStore( (store) => store.openVegaWalletDialog ); - - const getStartedNeeded = - onBoardingViewed !== 'true' && - currentStep && - currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP; + const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz'; + const currentStep = useGetOnboardingStep(); + const dismissed = useOnboardingStore((store) => store.dismissed); const wrapperClasses = classNames( 'flex flex-col py-4 px-6 gap-4 rounded', @@ -94,7 +84,7 @@ export const GetStarted = ({ lead }: Props) => { { 'mt-8': !lead } ); - if (getStartedNeeded) { + if (!dismissed) { return (
{lead &&

{lead}

} diff --git a/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts b/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts index 66adb9553..3f90fa82b 100644 --- a/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts +++ b/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts @@ -1,3 +1,5 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { depositsProvider } from '@vegaprotocol/deposits'; import { useDataProvider } from '@vegaprotocol/data-provider'; @@ -7,6 +9,29 @@ import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts'; import { positionsDataProvider } from '@vegaprotocol/positions'; import { useGlobalStore } from '../../stores'; +const ONBOARDING_STORAGE_KEY = 'vega_onboarding'; +export const useOnboardingStore = create<{ + dialogOpen: boolean; + dismissed: boolean; + dismiss: () => void; + setDialogOpen: (isOpen: boolean) => void; +}>()( + persist( + (set) => ({ + dialogOpen: true, + dismissed: false, + dismiss: () => set({ dismissed: true }), + setDialogOpen: (isOpen) => set({ dialogOpen: isOpen }), + }), + { + name: ONBOARDING_STORAGE_KEY, + partialize: (state) => ({ + dismissed: state.dismissed, + }), + } + ) +); + export enum OnboardingStep { ONBOARDING_UNKNOWN_STEP, ONBOARDING_WALLET_STEP, diff --git a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx index 1a2cedff6..f15471cff 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx @@ -1,40 +1,24 @@ import { t } from '@vegaprotocol/i18n'; import { GetStarted } from './get-started'; -import { TradingButton } from '@vegaprotocol/ui-toolkit'; -import { useNavigate } from 'react-router-dom'; +import { TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; import { Links, Routes } from '../../pages/client-router'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; import type { ReactNode } from 'react'; -import { useOnboardingStore } from './welcome-dialog'; -import { useMarketList } from '@vegaprotocol/markets'; -import { isMarketActive } from '../../lib/utils'; -import orderBy from 'lodash/orderBy'; -import { priceChangePercentage } from '@vegaprotocol/utils'; +import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets'; +import { useOnboardingStore } from './use-get-onboarding-step'; export const WelcomeDialogContent = () => { const { VEGA_ENV } = useEnvironment(); - - const dismiss = useOnboardingStore((store) => store.dismiss); - const navigate = useNavigate(); - const { data } = useMarketList(); - const markets = orderBy( - data?.filter((m) => isMarketActive(m.state)) || [], - [ - (m) => { - if (!m.candles?.length) return 0; - return Number(priceChangePercentage(m.candles.map((c) => c.close))); - }, - ], - ['desc'] + const setOnboardingDialog = useOnboardingStore( + (store) => store.setDialogOpen ); - const explore = () => { - const marketId = markets?.[0].id ?? ''; - const link = marketId - ? Links[Routes.MARKET](marketId) - : Links[Routes.MARKETS](); - navigate(link); - dismiss(); - }; + + const { data } = useTopTradedMarkets(); + const marketId = data && data[0]?.id; + const link = marketId + ? Links[Routes.MARKET](marketId) + : Links[Routes.MARKETS](); + const lead = VEGA_ENV === Networks.MAINNET ? t('Start trading on the worlds most advanced decentralised exchange.') @@ -43,7 +27,7 @@ export const WelcomeDialogContent = () => { ); return (
-
+
    } @@ -65,15 +49,16 @@ export const WelcomeDialogContent = () => { )} />
- setOnboardingDialog(false)} className="block w-full" data-testid="browse-markets-button" > {t('Explore')} - +
-
+
@@ -90,10 +75,10 @@ const ListItemContent = ({ text: string; }) => { return ( -
  • -
    {icon}
    +
  • +
    {icon}
    -

    {title}

    +

    {title}

    {text}

  • diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx index 7f7cbb06e..0e40e7333 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -1,133 +1,32 @@ -import { useNavigate } from 'react-router-dom'; -import type { Toast } from '@vegaprotocol/ui-toolkit'; -import { Dialog, Intent, useToasts } from '@vegaprotocol/ui-toolkit'; +import { Dialog, Intent } from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/i18n'; -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; import { useEnvironment } from '@vegaprotocol/environment'; -import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { WelcomeDialogContent } from './welcome-dialog-content'; -import { Links, Routes } from '../../pages/client-router'; -import { useGlobalStore } from '../../stores'; -import { - useGetOnboardingStep, - OnboardingStep, -} from './use-get-onboarding-step'; -import * as constants from '../constants'; -import { TelemetryApproval } from './telemetry-approval'; -import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval'; -import { useCallback } from 'react'; +import { useOnboardingStore } from './use-get-onboarding-step'; -const ONBOARDING_STORAGE_KEY = 'vega_onboarding_dismiss_store'; -export const useOnboardingStore = create<{ - dismissed: boolean; - dismiss: () => void; -}>()( - persist( - (set) => ({ - dismissed: false, - dismiss: () => set(() => ({ dismissed: true })), - }), - { - name: ONBOARDING_STORAGE_KEY, - } - ) -); - -const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id'; export const WelcomeDialog = () => { const { VEGA_ENV } = useEnvironment(); - const navigate = useNavigate(); - const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] = - useTelemetryApproval(); - const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY); - const dismiss = useOnboardingStore((store) => store.dismiss); const dismissed = useOnboardingStore((store) => store.dismissed); - const currentStep = useGetOnboardingStep(); - const isTelemetryPopupNeeded = - isTelemetryNeeded && - (onBoardingViewed === 'true' || - currentStep > OnboardingStep.ONBOARDING_ORDER_STEP); + const dialogOpen = useOnboardingStore((store) => store.dialogOpen); + const dismiss = useOnboardingStore((store) => store.dismiss); - const isOnboardingDialogNeeded = - onBoardingViewed !== 'true' && - currentStep && - currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP && - !dismissed; - const marketId = useGlobalStore((store) => store.marketId); - - const onClose = () => { - if (isTelemetryPopupNeeded) { - closeTelemetry(); - } else { - const link = marketId - ? Links[Routes.MARKET](marketId) - : Links[Routes.HOME](); - navigate(link); - dismiss(); - } - }; - - const [setToast, hasToast, removeToast] = useToasts((store) => [ - store.setToast, - store.hasToast, - store.remove, - ]); - const onApprovalClose = useCallback(() => { - closeTelemetry(); - removeToast(TELEMETRY_APPROVAL_TOAST_ID); - }, [removeToast, closeTelemetry]); - - const setTelemetryApprovalAndClose = useCallback( - (value: string) => { - setTelemetryValue(value); - onApprovalClose(); - }, - [setTelemetryValue, onApprovalClose] - ); - - if (isTelemetryPopupNeeded) { - const toast: Toast = { - id: TELEMETRY_APPROVAL_TOAST_ID, - intent: Intent.Primary, - content: ( - <> -

    - {t('Improve vega console')} -

    - - - ), - onClose: onApprovalClose, - }; - if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) { - setToast(toast); - } - return; - } - - const title = ( - - {t('Console')}{' '} - - {VEGA_ENV} - - - ); - - return isOnboardingDialogNeeded ? ( + return ( + {t('Console')}{' '} + + {VEGA_ENV} + + + } size="medium" - onChange={onClose} + onChange={() => dismiss()} intent={Intent.None} dataTestId="welcome-dialog" > - ) : null; + ); }; diff --git a/apps/trading/lib/hooks/use-top-traded-markets.tsx b/apps/trading/lib/hooks/use-top-traded-markets.tsx new file mode 100644 index 000000000..38cd3276b --- /dev/null +++ b/apps/trading/lib/hooks/use-top-traded-markets.tsx @@ -0,0 +1,13 @@ +import orderBy from 'lodash/orderBy'; +import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets'; +import { isMarketActive } from '../utils'; + +export const useTopTradedMarkets = () => { + const { data, loading, error } = useMarketList(); + + const activeMarkets = data?.filter((m) => isMarketActive(m.state)); + const marketsByTopTraded = data + ? orderBy(activeMarkets, (m) => calcTradedFactor(m), 'desc') + : undefined; + return { data: marketsByTopTraded, loading, error }; +}; diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index c3e76a848..188c9f889 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -49,6 +49,7 @@ import { import { ViewingBanner } from '../components/viewing-banner'; import { NavHeader } from '../components/navbar/nav-header'; import { Routes as AppRoutes } from './client-router'; +import { Telemetry } from '../components/telemetry'; const DEFAULT_TITLE = t('Welcome to Vega trading!'); @@ -125,6 +126,7 @@ function AppBody({ Component }: AppProps) { +
    ); } diff --git a/libs/cypress/src/lib/commands/vega-wallet-connect.ts b/libs/cypress/src/lib/commands/vega-wallet-connect.ts index 5ca985bdd..64d430a8f 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-connect.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-connect.ts @@ -61,10 +61,15 @@ export function addVegaWalletConnect() { }); } +const onboardingViewedState = { state: { dismissed: true }, version: 0 }; + export function addSetVegaWallet() { Cypress.Commands.add('setVegaWallet', () => { cy.window().then((win) => { - win.localStorage.setItem('vega_onboarding_viewed', 'true'); + win.localStorage.setItem( + 'vega_onboarding', + JSON.stringify(onboardingViewedState) + ); win.localStorage.setItem('vega_telemetry_approval', 'false'); win.localStorage.setItem('vega_telemetry_viewed', 'true'); win.localStorage.setItem( @@ -82,7 +87,10 @@ export function addSetVegaWallet() { export function addSetOnBoardingViewed() { Cypress.Commands.add('setOnBoardingViewed', () => { cy.window().then((win) => { - win.localStorage.setItem('vega_onboarding_viewed', 'true'); + win.localStorage.setItem( + 'vega_onboarding', + JSON.stringify(onboardingViewedState) + ); win.localStorage.setItem('vega_telemetry_approval', 'false'); win.localStorage.setItem('vega_telemetry_viewed', 'true'); }); diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx index 31aae4fc8..74c558b9d 100644 --- a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx +++ b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx @@ -1,7 +1,8 @@ export const IconEyeOff = ({ size = 16 }: { size: number }) => { return ( - + + ); }; diff --git a/libs/ui-toolkit/src/components/trading-button/trading-button.tsx b/libs/ui-toolkit/src/components/trading-button/trading-button.tsx index 48c6ac9b2..8ffe55cdc 100644 --- a/libs/ui-toolkit/src/components/trading-button/trading-button.tsx +++ b/libs/ui-toolkit/src/components/trading-button/trading-button.tsx @@ -6,6 +6,7 @@ import type { ReactNode, } from 'react'; import { Intent } from '../../utils/intent'; +import { Link } from 'react-router-dom'; type TradingButtonProps = { size?: 'large' | 'medium' | 'small' | 'extra-small'; @@ -119,30 +120,22 @@ export const TradingButton = forwardRef< ) ); -export const TradingAnchorButton = forwardRef< - HTMLAnchorElement, - AnchorHTMLAttributes & TradingButtonProps ->( - ( - { - size = 'medium', - intent = Intent.None, - icon, - href, - children, - className, - subLabel, - ...props - }, - ref - ) => ( - - - - ) +export const TradingAnchorButton = ({ + size = 'medium', + intent = Intent.None, + icon, + href, + children, + className, + subLabel, + ...props +}: AnchorHTMLAttributes & + TradingButtonProps & { href: string }) => ( + + + );