From 2c386a7c16b7ff26e26e821c807ce84a37542292 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 24 Jan 2025 11:27:22 +0000 Subject: [PATCH] Add sample page Add hook. Add local state. Add global state with duck and selectors. Add context state with context and hook. --- ui/components/app/sample/sample-page.tsx | 95 +++++++++++++++++++ .../app/wallet-overview/coin-buttons.tsx | 26 +++++ ui/components/app/wallet-overview/index.scss | 4 + ui/ducks/index.js | 2 + ui/ducks/sample/sample.ts | 38 ++++++++ ui/helpers/constants/routes.ts | 2 + ui/hooks/sample/useSample.ts | 25 +++++ ui/hooks/sample/useSampleContext.tsx | 48 ++++++++++ ui/pages/index.js | 5 +- ui/pages/routes/routes.component.js | 3 + ui/selectors/sample.ts | 15 +++ 11 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 ui/components/app/sample/sample-page.tsx create mode 100644 ui/ducks/sample/sample.ts create mode 100644 ui/hooks/sample/useSample.ts create mode 100644 ui/hooks/sample/useSampleContext.tsx create mode 100644 ui/selectors/sample.ts diff --git a/ui/components/app/sample/sample-page.tsx b/ui/components/app/sample/sample-page.tsx new file mode 100644 index 000000000000..be5b96e4e895 --- /dev/null +++ b/ui/components/app/sample/sample-page.tsx @@ -0,0 +1,95 @@ +import React, { useCallback, useState } from 'react'; +import { + AlignItems, + BackgroundColor, + IconColor, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { + Button, + ButtonIcon, + ButtonIconSize, + IconName, + Text, +} from '../../component-library'; +import { Content, Header, Page } from '../../multichain/pages/page'; +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { useHistory } from 'react-router-dom'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useSample } from '../../../hooks/sample/useSample'; +import { useSampleContext } from '../../../hooks/sample/useSampleContext'; + +export function SamplePage() { + const t = useI18nContext(); + const history = useHistory(); + const [localState, setLocalState] = useState(0); + const { globalCounter, updateGlobalCounter } = useSample(); + + const { counter: contextCounter, updateCounter: updateContextCounter } = + useSampleContext(); + + const handleLocalStateClick = useCallback(() => { + setLocalState((prev) => prev + 1); + }, [setLocalState]); + + const handleGlobalStateClick = useCallback(() => { + updateGlobalCounter(1); + }, [updateGlobalCounter]); + + const handleContextStateClick = useCallback(() => { + updateContextCounter(1); + }, [updateContextCounter]); + + return ( + +
history.push(DEFAULT_ROUTE)} + size={ButtonIconSize.Sm} + /> + } + > + + Sample Page + +
+ + {`Local React State: ${localState}`} + + {`Global Redux State: ${globalCounter}`} + + {`React Context State: ${contextCounter}`} + + +
+ ); +} diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index e960e71bfa29..c19d70ca6ee5 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -105,6 +105,7 @@ import { getCurrentChainId } from '../../../../shared/modules/selectors/networks ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; ///: END:ONLY_INCLUDE_IF +import { useSample } from '../../../hooks/sample/useSample'; type CoinButtonsProps = { account: InternalAccount; @@ -490,6 +491,12 @@ const CoinButtons = ({ ///: END:ONLY_INCLUDE_IF ]); + const { openSampleFeature } = useSample(); + + const handleSampleClick = useCallback(() => { + openSampleFeature(); + }, [openSampleFeature]); + return ( { @@ -615,6 +622,25 @@ const CoinButtons = ({ /> } + + + } + onClick={handleSampleClick} + label="Sample" + data-testid="token-overview-button-sample" + tooltipRender={(contents: React.ReactElement) => + generateTooltip('swapButton', contents) + } + /> ); }; diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss index 47dc40200e69..7e6b837f116c 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -125,6 +125,10 @@ border-radius: 18px; margin-top: 6px; } + + &__button { + width: 55px !important; + } } .token-overview { diff --git a/ui/ducks/index.js b/ui/ducks/index.js index deba3123916f..8496f41925a4 100644 --- a/ui/ducks/index.js +++ b/ui/ducks/index.js @@ -13,6 +13,7 @@ import bridgeReducer from './bridge/bridge'; import historyReducer from './history/history'; import rampsReducer from './ramps/ramps'; import confirmAlertsReducer from './confirm-alerts/confirm-alerts'; +import sampleReducer from './sample/sample'; export default combineReducers({ [AlertTypes.invalidCustomNetwork]: invalidCustomNetwork, @@ -30,4 +31,5 @@ export default combineReducers({ bridge: bridgeReducer, gas: gasReducer, localeMessages: localeMessagesReducer, + sample: sampleReducer, }); diff --git a/ui/ducks/sample/sample.ts b/ui/ducks/sample/sample.ts new file mode 100644 index 000000000000..821904b2415b --- /dev/null +++ b/ui/ducks/sample/sample.ts @@ -0,0 +1,38 @@ +export type SampleGlobalState = { + counter: number; +}; + +type UpdateSampleCounterAction = { + type: 'UPDATE_SAMPLE_COUNTER'; + amount: number; +}; + +type Action = UpdateSampleCounterAction; + +const INIT_STATE: SampleGlobalState = { + counter: 0, +}; + +export default function sampleReducer( + // eslint-disable-next-line @typescript-eslint/default-param-last + state: SampleGlobalState = INIT_STATE, + action: Action, +) { + switch (action.type) { + case 'UPDATE_SAMPLE_COUNTER': + return { + ...state, + counter: state.counter + action.amount, + }; + + default: + return state; + } +} + +export function updateSampleCounter(amount: number): UpdateSampleCounterAction { + return { + type: 'UPDATE_SAMPLE_COUNTER', + amount, + }; +} diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index ac21c32a2b1b..b51d2bb1ed7b 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -283,3 +283,5 @@ export const ONBOARDING_METAMETRICS = '/onboarding/metametrics'; export const INITIALIZE_EXPERIMENTAL_AREA = '/initialize/experimental-area'; export const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; ///: END:ONLY_INCLUDE_IF + +export const SAMPLE_ROUTE = '/sample'; diff --git a/ui/hooks/sample/useSample.ts b/ui/hooks/sample/useSample.ts new file mode 100644 index 000000000000..98e43aaa9364 --- /dev/null +++ b/ui/hooks/sample/useSample.ts @@ -0,0 +1,25 @@ +import { useCallback } from 'react'; +import { SAMPLE_ROUTE } from '../../helpers/constants/routes'; +import { useHistory } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectSampleCounter } from '../../selectors/sample'; +import { updateSampleCounter } from '../../ducks/sample/sample'; + +export function useSample() { + const history = useHistory(); + const dispatch = useDispatch(); + const globalCounter = useSelector(selectSampleCounter); + + const updateGlobalCounter = useCallback( + (amount: number) => { + dispatch(updateSampleCounter(amount)); + }, + [dispatch], + ); + + const openSampleFeature = useCallback(() => { + history.push(SAMPLE_ROUTE); + }, [history]); + + return { globalCounter, openSampleFeature, updateGlobalCounter }; +} diff --git a/ui/hooks/sample/useSampleContext.tsx b/ui/hooks/sample/useSampleContext.tsx new file mode 100644 index 000000000000..1893c6b987e3 --- /dev/null +++ b/ui/hooks/sample/useSampleContext.tsx @@ -0,0 +1,48 @@ +import React, { + ReactElement, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +export type SampleContexType = { + counter: number; + updateCounter: (amount: number) => void; +}; + +export const SampleContext = createContext( + undefined, +); + +export function SampleContextProvider({ children }: { children: ReactElement }) { + const [counter, setCounter] = useState(0); + + const updateCounter = useCallback((amount: number) => { + setCounter((prevCounter) => prevCounter + amount); + }, []); + + const value = useMemo( + () => ({ + counter, + updateCounter, + }), + [counter], + ); + + return {children} + +}; + +export function useSampleContext(): SampleContexType { + const context = useContext(SampleContext); + + if (!context) { + throw new Error( + 'useSampleContext must be used within a SampleContextProvider', + ); + } + + return context; +}; diff --git a/ui/pages/index.js b/ui/pages/index.js index 8c06c0f7429a..a10e997552d7 100644 --- a/ui/pages/index.js +++ b/ui/pages/index.js @@ -12,6 +12,7 @@ import { import { MetamaskNotificationsProvider } from '../contexts/metamask-notifications'; import { AssetPollingProvider } from '../contexts/assetPolling'; import { MetamaskIdentityProvider } from '../contexts/identity'; +import { SampleContextProvider } from '../hooks/sample/useSampleContext'; import ErrorPage from './error-page/error-page.component'; import Routes from './routes'; @@ -54,7 +55,9 @@ class Index extends PureComponent { - + + + diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 92827ca5eef7..77d54d6c35a1 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -62,6 +62,7 @@ import { NOTIFICATIONS_SETTINGS_ROUTE, CROSS_CHAIN_SWAP_ROUTE, CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE, + SAMPLE_ROUTE, } from '../../helpers/constants/routes'; import { @@ -93,6 +94,7 @@ import { isCorrectDeveloperTransactionType, isCorrectSignatureApprovalType, } from '../../../shared/lib/confirmation.utils'; +import { SamplePage } from '../../components/app/sample/sample-page'; import { getConnectingLabel, hideAppHeader, @@ -405,6 +407,7 @@ export default class Routes extends Component { component={ReviewPermissions} exact /> + diff --git a/ui/selectors/sample.ts b/ui/selectors/sample.ts new file mode 100644 index 000000000000..a062ec76f774 --- /dev/null +++ b/ui/selectors/sample.ts @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; +import { SampleGlobalState } from '../ducks/sample/sample'; + +type State = { + sample: SampleGlobalState; +}; + +export function selectSample(state: State): SampleGlobalState { + return state.sample; +} + +export const selectSampleCounter = createSelector( + selectSample, + (sample) => sample.counter, +);