From c14e57cfd5cc2837386153c551437ee351105017 Mon Sep 17 00:00:00 2001 From: macqbat Date: Tue, 13 Dec 2022 14:31:28 +0100 Subject: [PATCH] feat(2146): adjust and refactor welcome dialogs (#2384) * feat: adjust and refactor welcome dialogs * feat: adjust and refactor welcome dialogs - add int tests * feat: adjust and refactor welcome dialogs - small fixes and imprvments * feat: adjust and refactor welcome dialogs - fix a typo * feat: adjust and refactor welcome dialogs - fix a property name * feat: adjust and refactor welcome dialogs - fix an unit test --- apps/trading-e2e/src/integration/home.cy.ts | 65 ++++++- apps/trading/client-pages/home/home.tsx | 6 +- apps/trading/client-pages/market/market.tsx | 35 +--- apps/trading/client-pages/markets/markets.tsx | 4 +- apps/trading/components/constants.ts | 8 + .../components/risk-notice-dialog/index.tsx | 1 - .../select-market/select-market.spec.tsx | 25 +-- .../select-market/select-market.tsx | 126 +------------ .../components/welcome-dialog/index.ts | 1 + .../welcome-dialog/proposed-markets.tsx | 79 ++++++++ .../risk-notice-dialog.spec.tsx | 49 +++-- .../risk-notice-dialog.tsx | 38 ++-- .../welcome-dialog/welcome-dialog-header.tsx | 11 ++ .../welcome-dialog/welcome-dialog.tsx | 70 ++++++++ .../welcome-landing-dialog.spec.tsx | 169 ++++++++++++++++++ .../welcome-dialog/welcome-landing-dialog.tsx | 132 ++++++++++++++ .../welcome-dialog/welcome-notice-dialog.tsx | 66 +++++++ .../components/welcome-notice/index.ts | 1 - .../welcome-notice/welcome-notice-dialog.tsx | 146 --------------- apps/trading/pages/dialogs-container.tsx | 6 +- apps/trading/setup-tests.ts | 1 + apps/trading/stores/global.ts | 6 - libs/react-helpers/src/lib/i18n.ts | 13 +- 23 files changed, 659 insertions(+), 399 deletions(-) delete mode 100644 apps/trading/components/risk-notice-dialog/index.tsx create mode 100644 apps/trading/components/welcome-dialog/index.ts create mode 100644 apps/trading/components/welcome-dialog/proposed-markets.tsx rename apps/trading/components/{risk-notice-dialog => welcome-dialog}/risk-notice-dialog.spec.tsx (65%) rename apps/trading/components/{risk-notice-dialog => welcome-dialog}/risk-notice-dialog.tsx (70%) create mode 100644 apps/trading/components/welcome-dialog/welcome-dialog-header.tsx create mode 100644 apps/trading/components/welcome-dialog/welcome-dialog.tsx create mode 100644 apps/trading/components/welcome-dialog/welcome-landing-dialog.spec.tsx create mode 100644 apps/trading/components/welcome-dialog/welcome-landing-dialog.tsx create mode 100644 apps/trading/components/welcome-dialog/welcome-notice-dialog.tsx delete mode 100644 apps/trading/components/welcome-notice/index.ts delete mode 100644 apps/trading/components/welcome-notice/welcome-notice-dialog.tsx diff --git a/apps/trading-e2e/src/integration/home.cy.ts b/apps/trading-e2e/src/integration/home.cy.ts index ece141d3b..64bd703e5 100644 --- a/apps/trading-e2e/src/integration/home.cy.ts +++ b/apps/trading-e2e/src/integration/home.cy.ts @@ -86,6 +86,14 @@ describe('home', { tags: '@regression' }, () => { }); } }); + + cy.getByTestId('welcome-notice-proposed-markets') + .children('div.pt-1.flex.justify-between') + .should('have.length', 3) + .each((item) => { + cy.wrap(item).getByTestId('external-link').should('exist'); + }); + cy.getByTestId('dialog-close').click(); cy.getByTestId(selectMarketOverlay).should('not.exist'); @@ -96,7 +104,7 @@ describe('home', { tags: '@regression' }, () => { }); }); - describe('market table should properly rendered', () => { + describe('market table should be properly rendered', () => { it('redirects to a default market with the landing dialog open', () => { const override = { marketsConnection: { @@ -161,4 +169,59 @@ describe('home', { tags: '@regression' }, () => { ); }); }); + + describe('no proposal found', () => { + it('there is a link to propose market', () => { + cy.mockGQL((req) => { + aliasQuery(req, 'ProposalsList', { + proposalsConnection: { + __typename: 'ProposalsConnection', + edges: null, + }, + }); + }); + cy.visit('/'); + cy.wait('@Markets'); + cy.wait('@MarketsData'); + cy.getByTestId(selectMarketOverlay) + .get('table tr') + .then((row) => { + expect(row.length >= 3).to.be.true; + }); + cy.getByTestId('external-link') + .contains('Propose a market') + .should('exist'); + }); + }); + + describe('no proposal nor markets found', () => { + it('there are welcome text and a link to propose market', () => { + cy.mockGQL((req) => { + const data = { + marketsConnection: { + __typename: 'MarketConnection', + edges: [], + }, + }; + aliasQuery(req, 'Markets', data); + aliasQuery(req, 'MarketsData', data); + aliasQuery(req, 'ProposalsList', { + proposalsConnection: { + __typename: 'ProposalsConnection', + edges: null, + }, + }); + }); + cy.visit('/'); + cy.wait('@Markets'); + cy.wait('@MarketsData'); + cy.getByTestId('welcome-notice-title').should( + 'contain.text', + 'Welcome to Console' + ); + cy.getByTestId('external-link') + .contains('Propose a market') + .should('exist'); + }); + }); }); diff --git a/apps/trading/client-pages/home/home.tsx b/apps/trading/client-pages/home/home.tsx index d88d4448f..694c2583b 100644 --- a/apps/trading/client-pages/home/home.tsx +++ b/apps/trading/client-pages/home/home.tsx @@ -17,8 +17,7 @@ export const Home = () => { const { data, error, loading } = useDataProvider({ dataProvider: marketsWithDataProvider, }); - const { riskNoticeDialog, update } = useGlobalStore((store) => ({ - riskNoticeDialog: store.riskNoticeDialog, + const { update } = useGlobalStore((store) => ({ update: store.update, })); @@ -29,7 +28,6 @@ export const Home = () => { useEffect(() => { if (data) { - update({ landingDialog: data.length > 0 }); const marketId = data[0]?.id; const marketName = data[0]?.tradableInstrument.instrument.name; const marketPrice = data[0]?.data?.markPrice @@ -50,7 +48,7 @@ export const Home = () => { navigate(`/markets/${EMPTY_MARKET_ID}`); } } - }, [data, navigate, riskNoticeDialog, update, pageTitle, updateTitle]); + }, [data, navigate, update, pageTitle, updateTitle]); return ( diff --git a/apps/trading/client-pages/market/market.tsx b/apps/trading/client-pages/market/market.tsx index 53f603d79..b91a6e302 100644 --- a/apps/trading/client-pages/market/market.tsx +++ b/apps/trading/client-pages/market/market.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import debounce from 'lodash/debounce'; -import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { addDecimalsFormatNumber, t, @@ -18,10 +17,8 @@ import type { import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list'; import { useGlobalStore, usePageTitleStore } from '../../stores'; import { TradeGrid, TradePanels } from './trade-grid'; -import { ColumnKind, SelectMarketDialog } from '../../components/select-market'; import { useNavigate, useParams } from 'react-router-dom'; import { EMPTY_MARKET_ID } from '../../components/constants'; -import { useWelcomeNoticeDialog } from '../../components/welcome-notice'; const calculatePrice = (markPrice?: string, decimalPlaces?: number) => { return markPrice && decimalPlaces @@ -47,21 +44,15 @@ export const Market = ({ const marketId = isEmpty ? undefined : params.marketId; const { w } = useWindowSize(); - const { landingDialog, riskNoticeDialog, update } = useGlobalStore( - (store) => ({ - landingDialog: store.landingDialog, - riskNoticeDialog: store.riskNoticeDialog, - update: store.update, - }) - ); + const { update } = useGlobalStore((store) => ({ + update: store.update, + })); const { pageTitle, updateTitle } = usePageTitleStore((store) => ({ pageTitle: store.pageTitle, updateTitle: store.updateTitle, })); - const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); - const onSelect = useCallback( (id: string) => { if (id && id !== marketId) { @@ -113,8 +104,6 @@ export const Market = ({ skip: !marketId || !data, }); - useWelcomeNoticeDialog(); - const tradeView = useMemo(() => { if (w > 960) { return ; @@ -140,23 +129,7 @@ export const Market = ({ if (!data && !isEmpty) { return {t('Market not found')}; } - return ( - <> - {tradeView} - - update({ landingDialog: isOpen }) - } - onSelect={onSelect} - onCellClick={(e, kind, value) => { - if (value && kind === ColumnKind.Asset) { - openAssetDetailsDialog(value, e.target as HTMLElement); - } - }} - /> - - ); + return <>{tradeView}; }} /> ); diff --git a/apps/trading/client-pages/markets/markets.tsx b/apps/trading/client-pages/markets/markets.tsx index 9b4e5e2c2..2f2edd5a4 100644 --- a/apps/trading/client-pages/markets/markets.tsx +++ b/apps/trading/client-pages/markets/markets.tsx @@ -2,15 +2,13 @@ import { useCallback } from 'react'; import { MarketsContainer } from '@vegaprotocol/market-list'; import { useGlobalStore } from '../../stores'; import { useNavigate } from 'react-router-dom'; -import { useWelcomeNoticeDialog } from '../../components/welcome-notice'; export const Markets = () => { const navigate = useNavigate(); const { update } = useGlobalStore((store) => ({ update: store.update })); - useWelcomeNoticeDialog(); const handleOnSelect = useCallback( (marketId: string) => { - update({ marketId, welcomeNoticeDialog: false }); + update({ marketId }); navigate(`/markets/${marketId}`); }, [update, navigate] diff --git a/apps/trading/components/constants.ts b/apps/trading/components/constants.ts index ba9724f04..739bbb424 100644 --- a/apps/trading/components/constants.ts +++ b/apps/trading/components/constants.ts @@ -1,2 +1,10 @@ +import { t } from '@vegaprotocol/react-helpers'; export const DEBOUNCE_UPDATE_TIME = 500; export const EMPTY_MARKET_ID = 'empty'; +export const RISK_ACCEPTED_KEY = 'vega-risk-accepted'; +export const MAINNET_WELCOME_HEADER = t( + 'Trade cash settled futures on the fully decentralised Vega network.' +); +export const TESTNET_WELCOME_HEADER = t( + 'Try out trading cash settled futures on the fully decentralised Vega network (Testnet).' +); diff --git a/apps/trading/components/risk-notice-dialog/index.tsx b/apps/trading/components/risk-notice-dialog/index.tsx deleted file mode 100644 index aefcffdc8..000000000 --- a/apps/trading/components/risk-notice-dialog/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './risk-notice-dialog'; diff --git a/apps/trading/components/select-market/select-market.spec.tsx b/apps/trading/components/select-market/select-market.spec.tsx index b4756c61c..7a78c8a5b 100644 --- a/apps/trading/components/select-market/select-market.spec.tsx +++ b/apps/trading/components/select-market/select-market.spec.tsx @@ -1,10 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import * as Schema from '@vegaprotocol/types'; -import { - SelectAllMarketsTableBody, - SelectMarketLandingTable, -} from './select-market'; +import { SelectAllMarketsTableBody } from './select-market'; import type { MarketWithCandles, @@ -173,24 +170,4 @@ describe('SelectMarket', () => { fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]); expect(onSelect).toHaveBeenCalledWith('1'); }); - - it('should call onSelect callback on SelectMarketLandingTable', () => { - const onSelect = jest.fn(); - const onCellClick = jest.fn(); - - render( - - - , - { wrapper: MockedProvider } - ); - fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]); - expect(onSelect).toHaveBeenCalledWith('1'); - fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]); - expect(onSelect).toHaveBeenCalledWith('2'); - }); }); diff --git a/apps/trading/components/select-market/select-market.tsx b/apps/trading/components/select-market/select-market.tsx index d273b7862..ad60e7fd0 100644 --- a/apps/trading/components/select-market/select-market.tsx +++ b/apps/trading/components/select-market/select-market.tsx @@ -1,18 +1,9 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useMarketList } from '@vegaprotocol/market-list'; import { positionsDataProvider } from '@vegaprotocol/positions'; import { t, useDataProvider } from '@vegaprotocol/react-helpers'; -import { - Dialog, - ExternalLink, - Icon, - Intent, - Link as UILink, - Loader, - Popover, -} from '@vegaprotocol/ui-toolkit'; +import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - import { columnHeaders, columnHeadersPositionMarkets, @@ -24,7 +15,6 @@ import { SelectMarketTableRow, SelectMarketTableRowSplash, } from './select-market-table'; - import type { ReactNode } from 'react'; import type { MarketWithCandles, @@ -32,7 +22,6 @@ import type { } from '@vegaprotocol/market-list'; import type { PositionFieldsFragment } from '@vegaprotocol/positions'; import type { Column, OnCellClickHandler } from './select-market-columns'; -import { Link } from 'react-router-dom'; import { DApp, TOKEN_NEW_MARKET_PROPOSAL, @@ -40,48 +29,7 @@ import { } from '@vegaprotocol/environment'; import { useGlobalStore } from '../../stores'; -type Market = MarketWithCandles & MarketWithData; - -export const SelectMarketLandingTable = ({ - markets, - onSelect, - onCellClick, -}: { - markets: Market[] | null; - onSelect: (id: string) => void; - onCellClick: OnCellClickHandler; -}) => { - return ( - <> -
- - - - - - {markets?.map((market, i) => ( - - ))} - -
-
-
- - {'Or view full market list'} - -
- - ); -}; +export type Market = MarketWithCandles & MarketWithData; export const SelectAllMarketsTableBody = ({ markets, @@ -265,71 +213,3 @@ const TableTitle = ({ children }: { children: ReactNode }) => { ); }; - -export const SelectMarketDialog = ({ - dialogOpen, - setDialogOpen, - onSelect, - onCellClick, -}: { - dialogOpen: boolean; - setDialogOpen: (open: boolean) => void; - title?: string; - onSelect: (id: string) => void; - onCellClick: OnCellClickHandler; -}) => { - const onSelectMarket = (id: string) => { - onSelect(id); - setDialogOpen(false); - }; - - return ( - setDialogOpen(false)} - size="small" - > - - - ); -}; - -interface LandingDialogContainerProps { - onSelect: (id: string) => void; - onCellClick: OnCellClickHandler; -} - -const LandingDialogContainer = ({ - onSelect, - onCellClick, -}: LandingDialogContainerProps) => { - const { data, loading, error } = useMarketList(); - if (error) { - return ( -
-

{t('Failed to load markets')}

-
- ); - } - - if (loading) { - return ( -
-

{t('Loading...')}

-
- ); - } - - return ( - - ); -}; diff --git a/apps/trading/components/welcome-dialog/index.ts b/apps/trading/components/welcome-dialog/index.ts new file mode 100644 index 000000000..c7ff6c38a --- /dev/null +++ b/apps/trading/components/welcome-dialog/index.ts @@ -0,0 +1 @@ +export * from './welcome-dialog'; diff --git a/apps/trading/components/welcome-dialog/proposed-markets.tsx b/apps/trading/components/welcome-dialog/proposed-markets.tsx new file mode 100644 index 000000000..16931406a --- /dev/null +++ b/apps/trading/components/welcome-dialog/proposed-markets.tsx @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; +import { t, useDataProvider } from '@vegaprotocol/react-helpers'; +import { proposalsListDataProvider } from '@vegaprotocol/governance'; +import take from 'lodash/take'; +import * as Types from '@vegaprotocol/types'; +import { ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { + DApp, + TOKEN_NEW_MARKET_PROPOSAL, + TOKEN_PROPOSAL, + TOKEN_PROPOSALS, + useLinks, +} from '@vegaprotocol/environment'; + +export const ProposedMarkets = () => { + const variables = useMemo(() => { + return { + proposalType: Types.ProposalType.TYPE_NEW_MARKET, + }; + }, []); + const { data } = useDataProvider({ + dataProvider: proposalsListDataProvider, + variables, + skipUpdates: true, + }); + + const newMarkets = take( + (data || []).filter((proposal) => + [ + Types.ProposalState.STATE_OPEN, + Types.ProposalState.STATE_PASSED, + Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE, + ].includes(proposal.state) + ), + 3 + ).map((proposal) => ({ + id: proposal.id, + displayName: + proposal.terms.change.__typename === 'NewMarket' && + proposal.terms.change.instrument.code, + })); + + const tokenLink = useLinks(DApp.Token); + return useMemo( + () => ( +
+ {newMarkets.length > 0 ? ( + <> +

+ {t('Proposed markets')} +

+
+ {newMarkets.map(({ displayName, id }, i) => ( +
+
{displayName}
+
+ + {t('View or vote')} + +
+
+ ))} +
+ + {t('View all proposed markets')} + + + ) : ( + + {t('Propose a market')} + + )} +
+ ), + [newMarkets, tokenLink] + ); +}; diff --git a/apps/trading/components/risk-notice-dialog/risk-notice-dialog.spec.tsx b/apps/trading/components/welcome-dialog/risk-notice-dialog.spec.tsx similarity index 65% rename from apps/trading/components/risk-notice-dialog/risk-notice-dialog.spec.tsx rename to apps/trading/components/welcome-dialog/risk-notice-dialog.spec.tsx index 280fa89d7..a7976e417 100644 --- a/apps/trading/components/risk-notice-dialog/risk-notice-dialog.spec.tsx +++ b/apps/trading/components/welcome-dialog/risk-notice-dialog.spec.tsx @@ -1,15 +1,9 @@ +import { BrowserRouter } from 'react-router-dom'; import { render, screen, fireEvent } from '@testing-library/react'; -import { RiskNoticeDialog } from './risk-notice-dialog'; +import { MockedProvider } from '@apollo/client/testing'; import { Networks, EnvironmentProvider } from '@vegaprotocol/environment'; -import { useGlobalStore } from '../../stores'; - -beforeEach(() => { - localStorage.clear(); - useGlobalStore.setState((state) => ({ - ...state, - riskNoticeDialog: false, - })); -}); +import { RiskNoticeDialog } from './risk-notice-dialog'; +import { WelcomeDialog } from './welcome-dialog'; const mockEnvDefinitions = { VEGA_CONFIG_URL: 'https://config.url', @@ -18,6 +12,12 @@ const mockEnvDefinitions = { }; describe('Risk notice dialog', () => { + const introText = 'Regulation may apply to use of this app'; + const mockOnClose = jest.fn(); + afterEach(() => { + jest.clearAllMocks(); + }); + it.each` assertion | network ${'displays'} | ${Networks.MAINNET} @@ -33,46 +33,39 @@ describe('Risk notice dialog', () => { definitions={{ ...mockEnvDefinitions, VEGA_ENV: network }} config={{ hosts: [] }} > - - + + + + , + { wrapper: BrowserRouter } ); if (assertion === 'displays') { // eslint-disable-next-line jest/no-conditional-expect - expect(screen.queryByText('WARNING')).toBeInTheDocument(); + expect(screen.queryByText(introText)).toBeInTheDocument(); } else { // eslint-disable-next-line jest/no-conditional-expect - expect(screen.queryByText('WARNING')).not.toBeInTheDocument(); + expect(screen.queryByText(introText)).not.toBeInTheDocument(); } } ); it("doesn't display the risk notice when previously acknowledged", () => { - const { rerender } = render( + render( - + ); - expect(screen.queryByText('WARNING')).toBeInTheDocument(); + expect(screen.queryByText(introText)).toBeInTheDocument(); const button = screen.getByRole('button', { name: 'I understand, Continue', }); fireEvent.click(button); - - rerender( - - - - ); - - expect(screen.queryByText('WARNING')).not.toBeInTheDocument(); + expect(mockOnClose).toHaveBeenCalled(); }); }); diff --git a/apps/trading/components/risk-notice-dialog/risk-notice-dialog.tsx b/apps/trading/components/welcome-dialog/risk-notice-dialog.tsx similarity index 70% rename from apps/trading/components/risk-notice-dialog/risk-notice-dialog.tsx rename to apps/trading/components/welcome-dialog/risk-notice-dialog.tsx index de3acecad..1bc65266d 100644 --- a/apps/trading/components/risk-notice-dialog/risk-notice-dialog.tsx +++ b/apps/trading/components/welcome-dialog/risk-notice-dialog.tsx @@ -1,34 +1,20 @@ -import { useEffect } from 'react'; +import { useCallback } from 'react'; import { t } from '@vegaprotocol/react-helpers'; -import { Dialog, Button } from '@vegaprotocol/ui-toolkit'; +import { Button } from '@vegaprotocol/ui-toolkit'; import { LocalStorage } from '@vegaprotocol/react-helpers'; -import { useEnvironment, Networks } from '@vegaprotocol/environment'; -import { useGlobalStore } from '../../stores'; +import { RISK_ACCEPTED_KEY } from '../constants'; -export const RISK_ACCEPTED_KEY = 'vega-risk-accepted'; - -export const RiskNoticeDialog = () => { - const { riskNoticeDialog, update } = useGlobalStore((store) => ({ - riskNoticeDialog: store.riskNoticeDialog, - update: store.update, - })); - const { VEGA_ENV } = useEnvironment(); - - useEffect(() => { - const isRiskAccepted = LocalStorage.getItem(RISK_ACCEPTED_KEY) === 'true'; - if (!isRiskAccepted && VEGA_ENV === Networks.MAINNET) { - update({ riskNoticeDialog: true }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [update, VEGA_ENV]); - - const handleAcceptRisk = () => { - update({ riskNoticeDialog: false }); +interface Props { + onClose: () => void; +} +export const RiskNoticeDialog = ({ onClose }: Props) => { + const handleAcceptRisk = useCallback(() => { + onClose(); LocalStorage.setItem(RISK_ACCEPTED_KEY, 'true'); - }; + }, [onClose]); return ( - + <>

{t('Regulation may apply to use of this app')}

@@ -46,6 +32,6 @@ export const RiskNoticeDialog = () => { )}

-
+ ); }; diff --git a/apps/trading/components/welcome-dialog/welcome-dialog-header.tsx b/apps/trading/components/welcome-dialog/welcome-dialog-header.tsx new file mode 100644 index 000000000..de626e68e --- /dev/null +++ b/apps/trading/components/welcome-dialog/welcome-dialog-header.tsx @@ -0,0 +1,11 @@ +import { Networks, useEnvironment } from '@vegaprotocol/environment'; +import * as constants from '../constants'; + +export const WelcomeDialogHeader = () => { + const { VEGA_ENV } = useEnvironment(); + const header = + VEGA_ENV === Networks.MAINNET + ? constants.MAINNET_WELCOME_HEADER + : constants.TESTNET_WELCOME_HEADER; + return

{header}

; +}; diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx new file mode 100644 index 000000000..97be3da40 --- /dev/null +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -0,0 +1,70 @@ +import React, { useMemo, useState, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { Dialog } from '@vegaprotocol/ui-toolkit'; +import { + t, + useDataProvider, + useLocalStorage, +} from '@vegaprotocol/react-helpers'; +import { activeMarketsProvider } from '@vegaprotocol/market-list'; +import { useEnvironment, Networks } from '@vegaprotocol/environment'; +import * as constants from '../constants'; +import { RiskNoticeDialog } from './risk-notice-dialog'; +import { WelcomeNoticeDialog } from './welcome-notice-dialog'; +import { WelcomeLandingDialog } from './welcome-landing-dialog'; + +interface DialogConfig { + open?: boolean; + content: React.ReactNode; + title?: string; + size?: 'small' | 'medium'; + onClose: () => void; +} +export const WelcomeDialog = () => { + const { pathname } = useLocation(); + const { VEGA_ENV } = useEnvironment(); + const [dialog, setDialog] = useState(null); + const onClose = useCallback(() => { + setDialog(null); + }, [setDialog]); + const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY); + + const { data } = useDataProvider({ + dataProvider: activeMarketsProvider, + }); + + useMemo(() => { + switch (true) { + case riskAccepted !== 'true' && VEGA_ENV === Networks.MAINNET: + setDialog({ + content: , + title: t('WARNING'), + size: 'medium', + onClose, + }); + break; + case pathname === '/' && data?.length === 0: + setDialog({ + content: , + onClose, + }); + break; + case pathname === '/' && (data?.length || 0) > 0: + setDialog({ + content: , + onClose, + }); + break; + } + }, [onClose, data?.length, riskAccepted, pathname, VEGA_ENV, setDialog]); + return dialog ? ( + + {dialog.content} + + ) : null; +}; diff --git a/apps/trading/components/welcome-dialog/welcome-landing-dialog.spec.tsx b/apps/trading/components/welcome-dialog/welcome-landing-dialog.spec.tsx new file mode 100644 index 000000000..cf1c3089d --- /dev/null +++ b/apps/trading/components/welcome-dialog/welcome-landing-dialog.spec.tsx @@ -0,0 +1,169 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import * as Schema from '@vegaprotocol/types'; +import type { + MarketWithCandles, + MarketWithData, + MarketData, +} from '@vegaprotocol/market-list'; +import { SelectMarketLandingTable } from './welcome-landing-dialog'; + +type Market = MarketWithCandles & MarketWithData; +type PartialMarket = Partial< + Omit & { data: Partial } +>; + +const MARKET_A: PartialMarket = { + __typename: 'Market', + id: '1', + decimalPlaces: 2, + tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '1', + code: 'ABCDEF', + name: 'ABCDEF 1-Day', + product: { + __typename: 'Future', + quoteName: 'ABCDEF', + settlementAsset: { + __typename: 'Asset', + id: 'asset-ABC', + decimals: 2, + symbol: 'ABC', + }, + }, + metadata: { + __typename: 'InstrumentMetadata', + tags: ['ABC', 'DEF'], + }, + }, + }, + fees: { + __typename: 'Fees', + factors: { + __typename: 'FeeFactors', + infrastructureFee: '0.01', + liquidityFee: '0.01', + makerFee: '0.01', + }, + }, + data: { + __typename: 'MarketData', + market: { + __typename: 'Market', + id: '1', + }, + markPrice: '90', + trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING, + marketState: Schema.MarketState.STATE_PENDING, + marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION, + indicativeVolume: '1000', + }, + candles: [ + { + __typename: 'Candle', + high: '100', + low: '10', + open: '10', + close: '80', + volume: '1000', + periodStart: '2022-11-01T15:49:00Z', + }, + { + __typename: 'Candle', + high: '10', + low: '1', + open: '1', + close: '100', + volume: '1000', + periodStart: '2022-11-01T15:50:00Z', + }, + ], +}; + +const MARKET_B: PartialMarket = { + __typename: 'Market', + id: '2', + decimalPlaces: 2, + positionDecimalPlaces: 0, + tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '2', + code: 'XYZ', + name: 'XYZ 1-Day', + product: { + __typename: 'Future', + quoteName: 'XYZ', + settlementAsset: { + __typename: 'Asset', + id: 'asset-XYZ', + decimals: 2, + symbol: 'XYZ', + }, + }, + metadata: { + __typename: 'InstrumentMetadata', + tags: ['XYZ'], + }, + }, + }, + fees: { + __typename: 'Fees', + factors: { + __typename: 'FeeFactors', + infrastructureFee: '0.01', + liquidityFee: '0.01', + makerFee: '0.01', + }, + }, + data: { + __typename: 'MarketData', + market: { + __typename: 'Market', + id: '2', + }, + markPrice: '123.123', + trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING, + marketState: Schema.MarketState.STATE_PENDING, + marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION, + indicativeVolume: '2000', + }, + candles: [ + { + __typename: 'Candle', + high: '100', + low: '10', + open: '10', + close: '80', + volume: '1000', + periodStart: '2022-11-01T15:49:00Z', + }, + ], +}; + +describe('WelcomeLandingDialog', () => { + it('should call onSelect callback on SelectMarketLandingTable', () => { + const onClose = jest.fn(); + + render( + + + , + { wrapper: MockedProvider } + ); + fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]); + expect(onClose).toHaveBeenCalled(); + fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/apps/trading/components/welcome-dialog/welcome-landing-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-landing-dialog.tsx new file mode 100644 index 000000000..cb09d0563 --- /dev/null +++ b/apps/trading/components/welcome-dialog/welcome-landing-dialog.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from 'react'; +import { useMarketList } from '@vegaprotocol/market-list'; +import { t } from '@vegaprotocol/react-helpers'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; +import { Link as UILink } from '@vegaprotocol/ui-toolkit'; +import type { Market, OnCellClickHandler } from '../select-market'; +import { + ColumnKind, + columns, + SelectMarketTableHeader, + SelectMarketTableRow, +} from '../select-market'; +import { WelcomeDialogHeader } from './welcome-dialog-header'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { EMPTY_MARKET_ID } from '../constants'; +import { useGlobalStore } from '../../stores'; +import { ProposedMarkets } from './proposed-markets'; + +export const SelectMarketLandingTable = ({ + markets, + onClose, +}: { + markets: Market[] | null; + onClose: () => void; +}) => { + const params = useParams(); + const navigate = useNavigate(); + const isEmpty = params.marketId === EMPTY_MARKET_ID; + const marketId = isEmpty ? undefined : params.marketId; + + const { update } = useGlobalStore((store) => ({ + update: store.update, + })); + + const onSelect = useCallback( + (id: string) => { + if (id && id !== marketId) { + update({ marketId: id }); + navigate(`/markets/${id}`); + } + }, + [marketId, update, navigate] + ); + + const onSelectMarket = useCallback( + (id: string) => { + onSelect(id); + onClose(); + }, + [onSelect, onClose] + ); + const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + const onCellClick = useCallback( + (e, kind, value) => { + if (value && kind === ColumnKind.Asset) { + openAssetDetailsDialog(value, e.target as HTMLElement); + } + }, + [openAssetDetailsDialog] + ); + const showProposed = (markets?.length || 0) <= 5; + return ( + <> +
+

+ {t('Select a market to get started...')} +

+ + + + + + {markets?.map((market, i) => ( + + ))} + +
+
+
+ onClose()} + > + {'Or view full market list'} + +
+ {showProposed && } + + ); +}; + +interface LandingDialogContainerProps { + onClose: () => void; +} + +export const WelcomeLandingDialog = ({ + onClose, +}: LandingDialogContainerProps) => { + const { data, loading, error } = useMarketList(); + if (error) { + return ( +
+

{t('Failed to load markets')}

+
+ ); + } + + if (loading) { + return ( +
+

{t('Loading...')}

+
+ ); + } + + return ( + <> + + + + ); +}; diff --git a/apps/trading/components/welcome-dialog/welcome-notice-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-notice-dialog.tsx new file mode 100644 index 000000000..73d343aa8 --- /dev/null +++ b/apps/trading/components/welcome-dialog/welcome-notice-dialog.tsx @@ -0,0 +1,66 @@ +import { ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/react-helpers'; +import { + BLOG, + DApp, + Networks, + TOKEN_NEW_MARKET_PROPOSAL, + TOKEN_PROPOSALS, + useEnvironment, + useLinks, +} from '@vegaprotocol/environment'; +import { ProposedMarkets } from './proposed-markets'; + +export const WelcomeNoticeDialog = () => { + const { VEGA_ENV } = useEnvironment(); + const tokenLink = useLinks(DApp.Token); + const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET); + const isMainnet = VEGA_ENV === Networks.MAINNET; + const networkName = isMainnet ? 'mainnet' : 'testnet'; + + return ( + <> +

+ {t('Welcome to Console')} +

+

+ {t( + 'Vega %s is now live, but markets need to be voted for before the can be traded on. In the meantime:', + [networkName] + )} +

+
    + {isMainnet && ( +
  • + + {t('Try out Console')} + + {t(' on Fairground, our Testnet')} +
  • + )} +
  • + + {t('View and vote for proposed markets')} + +
  • +
  • + + {t('Propose your own markets')} + +
  • +
  • + + {t('Read about the mainnet launch')} + +
  • +
+ + + ); +}; diff --git a/apps/trading/components/welcome-notice/index.ts b/apps/trading/components/welcome-notice/index.ts deleted file mode 100644 index c0c03403f..000000000 --- a/apps/trading/components/welcome-notice/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './welcome-notice-dialog'; diff --git a/apps/trading/components/welcome-notice/welcome-notice-dialog.tsx b/apps/trading/components/welcome-notice/welcome-notice-dialog.tsx deleted file mode 100644 index 60bfd3dd7..000000000 --- a/apps/trading/components/welcome-notice/welcome-notice-dialog.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Dialog, ExternalLink } from '@vegaprotocol/ui-toolkit'; -import { proposalsListDataProvider } from '@vegaprotocol/governance'; -import * as Types from '@vegaprotocol/types'; -import { t, useDataProvider } from '@vegaprotocol/react-helpers'; -import { useCallback, useEffect, useMemo } from 'react'; -import { useGlobalStore } from '../../stores'; -import take from 'lodash/take'; -import { activeMarketsProvider } from '@vegaprotocol/market-list'; -import { - BLOG, - DApp, - Networks, - TOKEN_NEW_MARKET_PROPOSAL, - TOKEN_PROPOSAL, - TOKEN_PROPOSALS, - useLinks, -} from '@vegaprotocol/environment'; - -export const WelcomeNoticeDialog = () => { - const [welcomeNoticeDialog, update] = useGlobalStore((store) => [ - store.welcomeNoticeDialog, - store.update, - ]); - - const onOpenChange = useCallback( - (isOpen: boolean) => { - update({ welcomeNoticeDialog: isOpen }); - }, - [update] - ); - - const variables = useMemo(() => { - return { - proposalType: Types.ProposalType.TYPE_NEW_MARKET, - }; - }, []); - - const { data } = useDataProvider({ - dataProvider: proposalsListDataProvider, - variables, - skipUpdates: true, - }); - - const newMarkets = take( - (data || []).filter((proposal) => - [ - Types.ProposalState.STATE_OPEN, - Types.ProposalState.STATE_PASSED, - Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE, - ].includes(proposal.state) - ), - 3 - ).map((proposal) => ({ - id: proposal.id, - displayName: - proposal.terms.change.__typename === 'NewMarket' && - proposal.terms.change.instrument.code, - })); - - const tokenLink = useLinks(DApp.Token); - const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET); - - const proposedMarkets = useMemo( - () => - newMarkets.length > 0 && ( -
-

- {t('Proposed markets')} -

-
- {newMarkets.map(({ displayName, id }, i) => ( -
-
{displayName}
-
- - {t('View or vote')} - -
-
- ))} -
- - {t('View all proposed markets')} - -
- ), - [newMarkets, tokenLink] - ); - - return ( - -

- {t('Welcome to Console')} -

-

- {t( - 'Vega mainnet is now live, but markets need to be voted for before the can be traded on. In the meantime:' - )} -

-
    -
  • - - {t('Try out Console')} - - {t(' on Fairground, our Testnet')} -
  • -
  • - - {t('View and vote for proposed markets')} - -
  • -
  • - - {t('Propose your own markets')} - -
  • -
  • - - {t('Read about the mainnet launch')} - -
  • -
- {proposedMarkets} -
- ); -}; - -export const useWelcomeNoticeDialog = () => { - const { update } = useGlobalStore((store) => ({ update: store.update })); - const { data } = useDataProvider({ - dataProvider: activeMarketsProvider, - }); - useEffect(() => { - if (data?.length === 0) { - update({ welcomeNoticeDialog: true }); - } - }, [data, update]); -}; diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx index ee5e12519..ed6329289 100644 --- a/apps/trading/pages/dialogs-container.tsx +++ b/apps/trading/pages/dialogs-container.tsx @@ -4,11 +4,10 @@ import { } from '@vegaprotocol/assets'; import { VegaConnectDialog } from '@vegaprotocol/wallet'; import { Connectors } from '../lib/vega-connectors'; -import { RiskNoticeDialog } from '../components/risk-notice-dialog'; import { WithdrawalDialog } from '@vegaprotocol/withdraws'; import { DepositDialog } from '@vegaprotocol/deposits'; import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3'; -import { WelcomeNoticeDialog } from '../components/welcome-notice'; +import { WelcomeDialog } from '../components/welcome-dialog'; const DialogsContainer = () => { const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore(); @@ -21,11 +20,10 @@ const DialogsContainer = () => { open={isOpen} onChange={setOpen} /> - + - ); }; diff --git a/apps/trading/setup-tests.ts b/apps/trading/setup-tests.ts index bcbaae582..17e7e61cb 100644 --- a/apps/trading/setup-tests.ts +++ b/apps/trading/setup-tests.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import 'jest-canvas-mock'; import ResizeObserver from 'resize-observer-polyfill'; global.ResizeObserver = ResizeObserver; diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index 455eabd87..493f53efd 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -3,10 +3,7 @@ import create from 'zustand'; interface GlobalStore { networkSwitcherDialog: boolean; - landingDialog: boolean; - riskNoticeDialog: boolean; marketId: string | null; - welcomeNoticeDialog: boolean; update: (store: Partial>) => void; } @@ -17,10 +14,7 @@ interface PageTitleStore { export const useGlobalStore = create((set) => ({ networkSwitcherDialog: false, - landingDialog: false, - riskNoticeDialog: false, marketId: LocalStorage.getItem('marketId') || null, - welcomeNoticeDialog: false, update: (state) => { set(state); if (state.marketId) { diff --git a/libs/react-helpers/src/lib/i18n.ts b/libs/react-helpers/src/lib/i18n.ts index f9044ee9c..ae43536d4 100644 --- a/libs/react-helpers/src/lib/i18n.ts +++ b/libs/react-helpers/src/lib/i18n.ts @@ -6,4 +6,15 @@ * @param str A * @returns str A */ -export const t = (str: string) => str; +export const t = (str: string, replacements?: string | string[]) => { + if (replacements) { + let i = 0; + return str.replace(/%s/g, () => { + return ( + (Array.isArray(replacements) ? replacements : [replacements])[i++] || + '%s' + ); + }); + } + return str; +};