From d08e74958019efae1e04a8001c9dbc2d56153f2d Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 27 Oct 2023 19:59:18 +0200 Subject: [PATCH 01/13] feat(staking): add activity tab --- apps/browser-extension-wallet/.env.defaults | 1 + apps/browser-extension-wallet/.env.example | 2 + packages/common/src/analytics/types.ts | 1 + .../src/features/activity/Activity.tsx | 1 + .../staking/src/features/activity/index.ts | 1 + .../src/features/i18n/translations/en.ts | 1 + packages/staking/src/features/i18n/types.ts | 1 + .../src/features/staking/Navigation.tsx | 44 ++++++++++++++----- .../src/features/staking/StakingView.tsx | 2 + .../stateMachine/commands.ts | 9 +++- .../stateMachine/processExpandedViewCases.ts | 32 ++++++++++++++ .../stateMachine/types.ts | 7 +++ 12 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 packages/staking/src/features/activity/Activity.tsx create mode 100644 packages/staking/src/features/activity/index.ts diff --git a/apps/browser-extension-wallet/.env.defaults b/apps/browser-extension-wallet/.env.defaults index fded3830dc..5525b46cb8 100644 --- a/apps/browser-extension-wallet/.env.defaults +++ b/apps/browser-extension-wallet/.env.defaults @@ -23,6 +23,7 @@ USE_ADA_HANDLE=true USE_DATA_CHECK=false USE_POSTHOG_ANALYTICS=true USE_COMBINED_PASSWORD_NAME_STEP_COMPONENT=false +USE_MULTI_DELEGATION_STAKING_ACTIVITY=false USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT=false USE_MATOMO_ANALYTICS_FOR_OPTED_OUT=false diff --git a/apps/browser-extension-wallet/.env.example b/apps/browser-extension-wallet/.env.example index f3897cd232..76bb6bead5 100644 --- a/apps/browser-extension-wallet/.env.example +++ b/apps/browser-extension-wallet/.env.example @@ -26,6 +26,8 @@ USE_POSTHOG_ANALYTICS=true USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT=false USE_MATOMO_ANALYTICS_FOR_OPTED_OUT=false USE_COMBINED_PASSWORD_NAME_STEP_COMPONENT=false +USE_MULTI_DELEGATION_STAKING_ACTIVITY=false + # In App URLs CATALYST_GOOGLE_PLAY_URL=https://play.google.com/store/apps/details?id=io.iohk.vitvoting CATALYST_APP_STORE_URL=https://apps.apple.com/fr/app/catalyst-voting/id1517473397?l=en diff --git a/packages/common/src/analytics/types.ts b/packages/common/src/analytics/types.ts index e0cac05cd9..e169c0c88b 100644 --- a/packages/common/src/analytics/types.ts +++ b/packages/common/src/analytics/types.ts @@ -56,6 +56,7 @@ export enum PostHogAction { StakingAboutStakingFaqClick = 'staking | about staking | faq | click', StakingMultiDelegationDedicatedBlogClick = 'staking | multi-delegation | dedicated blog | click', StakingMultiDelegationGotItClick = 'staking | multi-delegation | got it | click', + StakingActivityClick = 'staking | activity | click', StakingOverviewClick = 'staking | overview | click', StakingOverviewCopyAddressClick = 'staking | overview | copy address | click', StakingOverviewManageClick = 'staking | overview | manage | click', diff --git a/packages/staking/src/features/activity/Activity.tsx b/packages/staking/src/features/activity/Activity.tsx new file mode 100644 index 0000000000..c9d20953e1 --- /dev/null +++ b/packages/staking/src/features/activity/Activity.tsx @@ -0,0 +1 @@ +export const Activity = () => <>Activity placeholder; diff --git a/packages/staking/src/features/activity/index.ts b/packages/staking/src/features/activity/index.ts new file mode 100644 index 0000000000..cad213e19d --- /dev/null +++ b/packages/staking/src/features/activity/index.ts @@ -0,0 +1 @@ +export { Activity } from './Activity'; diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index 4fbd7cc4d6..8355763e61 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -159,6 +159,7 @@ export const en: Translations = { 'portfolioBar.maxPools': '(max {{maxPoolsCount}})', 'portfolioBar.next': 'Next', 'portfolioBar.selectedPools': '{{selectedPoolsCount}} pools selected', + 'root.nav.activityTitle': 'Activity', 'root.nav.browsePoolsTitle': 'Browse pools', 'root.nav.overviewTitle': 'Overview', 'root.nav.title': 'Staking Navigation', diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index e0d829fd97..12c83ee6e1 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -241,6 +241,7 @@ type KeysStructure = { root: { title: ''; nav: { + activityTitle: ''; browsePoolsTitle: ''; title: ''; overviewTitle: ''; diff --git a/packages/staking/src/features/staking/Navigation.tsx b/packages/staking/src/features/staking/Navigation.tsx index c672a5efc3..7d658b16c1 100644 --- a/packages/staking/src/features/staking/Navigation.tsx +++ b/packages/staking/src/features/staking/Navigation.tsx @@ -6,6 +6,7 @@ import { useOutsideHandles } from '../outside-handles-provider'; import { DelegationFlow, useDelegationPortfolioStore } from '../store'; export enum Page { + activity = 'activity', overview = 'overview', browsePools = 'browsePools', } @@ -19,23 +20,33 @@ const isValueAValidSubPage = (value: string): value is Page => Object.values { const { analytics } = useOutsideHandles(); const { activePage, portfolioMutators } = useDelegationPortfolioStore((store) => ({ - activePage: [ - DelegationFlow.Overview, - DelegationFlow.CurrentPoolDetails, - DelegationFlow.PortfolioManagement, - ].includes(store.activeDelegationFlow) - ? Page.overview - : Page.browsePools, + activePage: (() => { + const flowToPage: Record = { + [DelegationFlow.Activity]: Page.activity, + [DelegationFlow.Overview]: Page.overview, + [DelegationFlow.CurrentPoolDetails]: Page.overview, + [DelegationFlow.PortfolioManagement]: Page.overview, + [DelegationFlow.ChangingPreferences]: Page.browsePools, + [DelegationFlow.BrowsePools]: Page.browsePools, + [DelegationFlow.NewPortfolio]: Page.browsePools, + [DelegationFlow.PoolDetails]: Page.browsePools, + }; + return flowToPage[store.activeDelegationFlow]; + })(), portfolioMutators: store.mutators, })); const { t } = useTranslation(); const onValueChange = (value: string) => { if (!isValueAValidSubPage(value)) return; - analytics.sendEventToPostHog( - value === Page.overview ? PostHogAction.StakingOverviewClick : PostHogAction.StakingBrowsePoolsClick - ); + const pageToEventParams = { + [Page.activity]: ['GoToActivity', PostHogAction.StakingActivityClick] as const, + [Page.overview]: ['GoToOverview', PostHogAction.StakingOverviewClick] as const, + [Page.browsePools]: ['GoToBrowsePools', PostHogAction.StakingBrowsePoolsClick] as const, + }; + const [command, posthogEvent] = pageToEventParams[value]; + analytics.sendEventToPostHog(posthogEvent); portfolioMutators.executeCommand({ - type: value === Page.overview ? 'GoToOverview' : 'GoToBrowsePools', + type: command, }); }; @@ -62,6 +73,17 @@ export const Navigation = ({ children }: NavigationProps) => { tabIndex={0} highlightWidth="half" /> + {process.env.USE_MULTI_DELEGATION_STAKING_ACTIVITY === 'true' ? ( + + ) : ( + <> + )} {children(activePage)} diff --git a/packages/staking/src/features/staking/StakingView.tsx b/packages/staking/src/features/staking/StakingView.tsx index 1540a66465..021f96a418 100644 --- a/packages/staking/src/features/staking/StakingView.tsx +++ b/packages/staking/src/features/staking/StakingView.tsx @@ -1,4 +1,5 @@ import { Box, Text } from '@lace/ui'; +import { Activity } from 'features/activity'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { BrowsePools } from '../BrowsePools'; @@ -46,6 +47,7 @@ export const StakingView = () => { {activePage === Page.overview && } {activePage === Page.browsePools && } + {activePage === Page.activity && } )} diff --git a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/commands.ts b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/commands.ts index 416f966414..80bd93f600 100644 --- a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/commands.ts +++ b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/commands.ts @@ -39,6 +39,10 @@ export type ManagePortfolio = { type: 'ManagePortfolio'; }; +export type GoToActivity = { + type: 'GoToActivity'; +}; + export type GoToBrowsePools = { type: 'GoToBrowsePools'; }; @@ -96,12 +100,15 @@ export type DrawerFailure = { type: 'DrawerFailure'; }; -export type OverviewCommand = ShowDelegatedPoolDetails | ManagePortfolio | GoToBrowsePools; +export type ActivityCommand = GoToOverview | GoToBrowsePools; + +export type OverviewCommand = ShowDelegatedPoolDetails | ManagePortfolio | GoToBrowsePools | GoToActivity; export type BrowsePoolsCommand = | SelectPoolFromList | UnselectPoolFromList | ShowPoolDetailsFromList + | GoToActivity | GoToOverview | ClearSelections | CreateNewPortfolio; diff --git a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/processExpandedViewCases.ts b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/processExpandedViewCases.ts index 05c5ab2e79..e0d114decf 100644 --- a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/processExpandedViewCases.ts +++ b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/processExpandedViewCases.ts @@ -1,6 +1,7 @@ import { PERCENTAGE_SCALE_MAX } from '../constants'; import { atomicStateMutators } from './atomicStateMutators'; import { + ActivityCommand, AddStakePools, BeginSingleStaking, BrowsePoolsCommand, @@ -14,6 +15,7 @@ import { DrawerBack, DrawerContinue, DrawerFailure, + GoToActivity, GoToBrowsePools, GoToOverview, ManagePortfolio, @@ -48,6 +50,7 @@ import { DrawerManagementStep, ExpandedViewDelegationFlow, Handler, + StateActivity, StateBrowsePools, StateChangingPreferences, StateCurrentPoolDetails, @@ -72,6 +75,10 @@ export const processExpandedViewCases: Handler = (params) => { [DelegationFlow.Overview]: cases( { + GoToActivity: handler(({ state }) => ({ + ...state, + activeDelegationFlow: DelegationFlow.Activity, + })), GoToBrowsePools: handler(({ state }) => ({ ...state, activeDelegationFlow: DelegationFlow.BrowsePools, @@ -92,6 +99,27 @@ export const processExpandedViewCases: Handler = (params) => params.command.type, DelegationFlow.Overview ), + [DelegationFlow.Activity]: cases( + { + GoToBrowsePools: handler(({ state }) => ({ + ...state, + activeDelegationFlow: DelegationFlow.BrowsePools, + draftPortfolio: undefined, + pendingSelectedPortfolio: undefined, + viewedStakePool: undefined, + })), + GoToOverview: handler(({ state }) => ({ + ...state, + activeDelegationFlow: DelegationFlow.Overview, + activeDrawerStep: undefined, + draftPortfolio: undefined, + pendingSelectedPortfolio: undefined, + viewedStakePool: undefined, + })), + }, + params.command.type, + DelegationFlow.Activity + ), [DelegationFlow.BrowsePools]: cases( { ClearSelections: handler(({ state }) => ({ @@ -116,6 +144,10 @@ export const processExpandedViewCases: Handler = (params) => ...atomicStateMutators.beginNewPortfolioCreation({ selections: state.selectedPortfolio }), }; }), + GoToActivity: handler(({ state }) => ({ + ...state, + activeDelegationFlow: DelegationFlow.Activity, + })), GoToOverview: handler(({ state }) => ({ ...state, activeDelegationFlow: DelegationFlow.Overview, diff --git a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts index eebfd557b9..41b5639f85 100644 --- a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts +++ b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts @@ -31,6 +31,7 @@ export type CurrentPortfolioStakePool = PortfolioStakePoolBase & export enum DelegationFlow { Overview = 'Overview', BrowsePools = 'BrowsePools', + Activity = 'Activity', CurrentPoolDetails = 'CurrentPoolDetails', PoolDetails = 'PoolDetails', PortfolioManagement = 'PortfolioManagement', @@ -85,6 +86,11 @@ export type StateOverview = MakeState<{ viewedStakePool: undefined; }>; +export type StateActivity = MakeState<{ + activeDelegationFlow: DelegationFlow.Activity; + activeDrawerStep: undefined; +}>; + export type StateCurrentPoolDetails = MakeState<{ activeDrawerStep: DrawerDefaultStep.PoolDetails; activeDelegationFlow: DelegationFlow.CurrentPoolDetails; @@ -134,6 +140,7 @@ export type StateChangingPreferences = MakeState<{ }>; export type State = + | StateActivity | StateOverview | StateCurrentPoolDetails | StatePortfolioManagement From dcd5f83f651aa8871599b0e083de600c8331ef99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Korba=C5=A1?= Date: Mon, 6 Nov 2023 09:38:54 +0100 Subject: [PATCH 02/13] feat(staking): [LW-8877] Add activity tab to staking, show rewards list (#684) * feat(staking): add rewards list to Staking Activity tab * feat(staking): expose activity detail drawer in staking section to be used by staking activity * feat(staking): add "no staking activity yet" screen * fix(staking): align "no staking activity" component to center vertically --- .../MultiDelegationStakingPopup.tsx | 3 +- .../stores/slices/activity-detail-slice.ts | 4 +- .../components/MultiDelegationStaking.tsx | 43 +++++++++++++++++-- .../src/features/activity/Activity.tsx | 24 ++++++++++- .../activity/NoStakingActivity.css.ts | 13 ++++++ .../features/activity/NoStakingActivity.tsx | 17 ++++++++ .../src/features/activity/RewardsHistory.tsx | 29 +++++++++++++ .../helpers/getGroupedRewardsHistory.ts | 9 ++++ .../src/features/i18n/translations/en.ts | 2 + packages/staking/src/features/i18n/types.ts | 6 +++ .../outside-handles-provider/types.ts | 1 + .../src/features/staking/StakingView.tsx | 2 +- packages/staking/src/features/theme/colors.ts | 3 ++ 13 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 packages/staking/src/features/activity/NoStakingActivity.css.ts create mode 100644 packages/staking/src/features/activity/NoStakingActivity.tsx create mode 100644 packages/staking/src/features/activity/RewardsHistory.tsx create mode 100644 packages/staking/src/features/activity/helpers/getGroupedRewardsHistory.ts diff --git a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx index eca01a83f3..5c4ff1de87 100644 --- a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx +++ b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx @@ -73,7 +73,7 @@ export const MultiDelegationStakingPopup = (): JSX.Element => { name: 'AnalyticsEventNames.Staking.STAKING_MULTI_DELEGATION_POPUP' }); }, []); - const { walletActivities } = useWalletActivities({ sendAnalytics }); + const { walletActivities, walletActivitiesStatus } = useWalletActivities({ sendAnalytics }); const { fiatCurrency } = useCurrencyStore(); const { executeWithPassword } = useWalletManager(); const isLoadingNetworkInfo = useWalletStore(networkInfoStatusSelector); @@ -125,6 +125,7 @@ export const MultiDelegationStakingPopup = (): JSX.Element => { walletStoreNetworkInfo: networkInfo, walletStoreBlockchainProvider: blockchainProvider, walletStoreWalletActivities: walletActivities, + walletStoreWalletActivitiesStatus: walletActivitiesStatus, // TODO: LW-7575 make compactNumber reusable and not pass it here. compactNumber: compactNumberWithUnit, walletAddress, diff --git a/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts b/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts index 726edac529..83d4dd6936 100644 --- a/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts +++ b/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts @@ -89,12 +89,15 @@ const buildGetActivityDetail = walletInfo } = get(); + set({ fetchingActivityInfo: true }); + if (activityDetail.type === 'rewards') { const { activity, status, type } = activityDetail; const poolInfos = await getPoolInfos( activity.rewards.map(({ poolId }) => poolId), stakePoolProvider ); + set({ fetchingActivityInfo: false }); return { activity: { @@ -128,7 +131,6 @@ const buildGetActivityDetail = const { activity: tx, status, type, direction } = activityDetail; const walletAssets = await firstValueFrom(wallet.assetInfo$); const protocolParameters = await firstValueFrom(wallet.protocolParameters$); - set({ fetchingActivityInfo: true }); // Assets const assetIds = getTransactionAssetsId(tx.body.outputs); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/MultiDelegationStaking.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/MultiDelegationStaking.tsx index 4e331faa23..f99a17a5a9 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/MultiDelegationStaking.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/MultiDelegationStaking.tsx @@ -1,5 +1,5 @@ import { OutsideHandlesProvider, Staking } from '@lace/staking'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useAnalyticsContext, useBackgroundServiceAPIContext, @@ -17,6 +17,9 @@ import { MULTIDELEGATION_FIRST_VISIT_LS_KEY, MULTIDELEGATION_FIRST_VISIT_SINCE_PORTFOLIO_PERSISTENCE_LS_KEY } from '@utils/constants'; +import { ActivityDetail } from '../../activity'; +import { Drawer, DrawerNavigation } from '@lace/common'; +import { useTranslation } from 'react-i18next'; export const MultiDelegationStaking = (): JSX.Element => { const { theme } = useTheme(); @@ -38,7 +41,9 @@ export const MultiDelegationStaking = (): JSX.Element => { fetchNetworkInfo, networkInfo, blockchainProvider, - currentChain + currentChain, + activityDetail, + resetActivityState } = useWalletStore((state) => ({ getKeyAgentType: state.getKeyAgentType, inMemoryWallet: state.inMemoryWallet, @@ -50,8 +55,11 @@ export const MultiDelegationStaking = (): JSX.Element => { fetchNetworkInfo: state.fetchNetworkInfo, blockchainProvider: state.blockchainProvider, walletInfo: state.walletInfo, - currentChain: state.currentChain + currentChain: state.currentChain, + activityDetail: state.activityDetail, + resetActivityState: state.resetActivityState })); + const { t } = useTranslation(); const sendAnalytics = useCallback(() => { // TODO implement analytics for the new flow const analytics = { @@ -66,7 +74,7 @@ export const MultiDelegationStaking = (): JSX.Element => { name: 'AnalyticsEventNames.Staking.STAKING_MULTI_DELEGATION_BROWSER' }); }, []); - const { walletActivities } = useWalletActivities({ sendAnalytics }); + const { walletActivities, walletActivitiesStatus } = useWalletActivities({ sendAnalytics }); const { fiatCurrency } = useCurrencyStore(); const { executeWithPassword } = useWalletManager(); const [multidelegationFirstVisit, { updateLocalStorage: setMultidelegationFirstVisit }] = useLocalStorage( @@ -80,6 +88,11 @@ export const MultiDelegationStaking = (): JSX.Element => { const walletAddress = walletInfo.addresses?.[0].address?.toString(); const analytics = useAnalyticsContext(); + // Reset current transaction details and close drawer if network (blockchainProvider) has changed + useEffect(() => { + resetActivityState(); + }, [resetActivityState, blockchainProvider]); + return ( { walletStoreNetworkInfo: networkInfo, walletStoreBlockchainProvider: blockchainProvider, walletStoreWalletActivities: walletActivities, + walletStoreWalletActivitiesStatus: walletActivitiesStatus, // TODO: LW-7575 make compactNumber reusable and not pass it here. compactNumber: compactNumberWithUnit, multidelegationFirstVisit, @@ -118,6 +132,27 @@ export const MultiDelegationStaking = (): JSX.Element => { }} > + {/* + Note: Mounting the browser-extension activity details drawer here is just a workaround. + Ideally, the Drawer/Activity detail should be fully managed within the "Staking" component, + which contains the respective "Activity" section, but that would require moving/refactoring + large chunks of code, ATM tightly coupled with browser-extension state/logic, + to a separate package (core perhaps?). + */} + { + resetActivityState(); + }} + /> + } + > + {activityDetail && priceResult && } + ); }; diff --git a/packages/staking/src/features/activity/Activity.tsx b/packages/staking/src/features/activity/Activity.tsx index c9d20953e1..f6fb246cc4 100644 --- a/packages/staking/src/features/activity/Activity.tsx +++ b/packages/staking/src/features/activity/Activity.tsx @@ -1 +1,23 @@ -export const Activity = () => <>Activity placeholder; +import { StateStatus, useOutsideHandles } from 'features/outside-handles-provider'; +import { getGroupedRewardsActivities } from './helpers/getGroupedRewardsHistory'; +import { NoStakingActivity } from './NoStakingActivity'; +import { RewardsHistory } from './RewardsHistory'; + +export const Activity = () => { + const { walletStoreWalletActivitiesStatus: walletActivitiesStatus, walletStoreWalletActivities: walletActivities } = + useOutsideHandles(); + const groupedRewardsActivities = getGroupedRewardsActivities(walletActivities); + + return ( + <> + {walletActivitiesStatus === StateStatus.LOADED && groupedRewardsActivities.length === 0 ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/staking/src/features/activity/NoStakingActivity.css.ts b/packages/staking/src/features/activity/NoStakingActivity.css.ts new file mode 100644 index 0000000000..ebbfc6e56a --- /dev/null +++ b/packages/staking/src/features/activity/NoStakingActivity.css.ts @@ -0,0 +1,13 @@ +import { style } from '@lace/ui'; +import { theme } from 'features/theme'; + +export const sadFaceIcon = style({ + height: theme.spacing.$112, + width: theme.spacing.$112, +}); + +export const noActivityText = style({ + color: theme.colors.$activityNoActivityTextColor, + fontSize: theme.fontSizes.$14, + fontWeight: theme.fontWeights.$semibold, +}); diff --git a/packages/staking/src/features/activity/NoStakingActivity.tsx b/packages/staking/src/features/activity/NoStakingActivity.tsx new file mode 100644 index 0000000000..e2c928f3a9 --- /dev/null +++ b/packages/staking/src/features/activity/NoStakingActivity.tsx @@ -0,0 +1,17 @@ +import SadFaceIcon from '@lace/core/src/ui/assets/icons/sad-face.component.svg'; +import { Flex } from '@lace/ui'; +import { Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import * as styles from './NoStakingActivity.css'; + +export const NoStakingActivity = () => { + const { t } = useTranslation(); + return ( + + + + {t('activity.rewardsHistory.noStakingActivityYet')} + + + ); +}; diff --git a/packages/staking/src/features/activity/RewardsHistory.tsx b/packages/staking/src/features/activity/RewardsHistory.tsx new file mode 100644 index 0000000000..4f2a2cc45f --- /dev/null +++ b/packages/staking/src/features/activity/RewardsHistory.tsx @@ -0,0 +1,29 @@ +import { AssetActivityListProps, GroupedAssetActivityList } from '@lace/core'; +import { Box, Text } from '@lace/ui'; +import { Skeleton } from 'antd'; +import { StateStatus } from 'features/outside-handles-provider'; +import { useTranslation } from 'react-i18next'; + +const LACE_APP_ID = 'lace-app'; + +type RewardsHistoryProps = { + groupedRewardsActivities: AssetActivityListProps[]; + walletActivitiesStatus: StateStatus; +}; +export const RewardsHistory = ({ groupedRewardsActivities, walletActivitiesStatus }: RewardsHistoryProps) => { + const { t } = useTranslation(); + + return ( + <> + + {t('activity.rewardsHistory.title')} + + + + + + ); +}; diff --git a/packages/staking/src/features/activity/helpers/getGroupedRewardsHistory.ts b/packages/staking/src/features/activity/helpers/getGroupedRewardsHistory.ts new file mode 100644 index 0000000000..c665be690c --- /dev/null +++ b/packages/staking/src/features/activity/helpers/getGroupedRewardsHistory.ts @@ -0,0 +1,9 @@ +import { AssetActivityListProps } from '@lace/core'; + +export const getGroupedRewardsActivities = (walletActivities: AssetActivityListProps[]) => + walletActivities + .map((group) => ({ + ...group, + items: group.items.filter((item) => item.type === 'rewards'), + })) + .filter((group) => group.items.length > 0); diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index 8355763e61..4af359ddb3 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -1,6 +1,8 @@ import { Translations } from '../types'; export const en: Translations = { + 'activity.rewardsHistory.noStakingActivityYet': 'No staking activity yet.', + 'activity.rewardsHistory.title': 'History', 'browsePools.stakePoolTableBrowser.addPool': 'Add pool', 'browsePools.stakePoolTableBrowser.disabledTooltip': 'Maximum number of pools selected', 'browsePools.stakePoolTableBrowser.emptyMessage': 'No results matching your search', diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index 12c83ee6e1..31fc5b4e40 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -15,6 +15,12 @@ type KeysStructure = { close: ''; }; }; + activity: { + rewardsHistory: { + title: ''; + noStakingActivityYet: ''; + }; + }; browsePools: { stakePoolTableBrowser: { searchInputPlaceholder: ''; diff --git a/packages/staking/src/features/outside-handles-provider/types.ts b/packages/staking/src/features/outside-handles-provider/types.ts index 55d301f551..60165c488b 100644 --- a/packages/staking/src/features/outside-handles-provider/types.ts +++ b/packages/staking/src/features/outside-handles-provider/types.ts @@ -70,6 +70,7 @@ export type OutsideHandlesContextValue = { walletStoreGetKeyAgentType: () => string; walletStoreInMemoryWallet: Wallet.ObservableWallet; walletStoreWalletActivities: AssetActivityListProps[]; + walletStoreWalletActivitiesStatus: StateStatus; walletStoreWalletUICardanoCoin: Wallet.CoinId; walletManagerExecuteWithPassword: ( password: string, diff --git a/packages/staking/src/features/staking/StakingView.tsx b/packages/staking/src/features/staking/StakingView.tsx index 021f96a418..4abae0fd94 100644 --- a/packages/staking/src/features/staking/StakingView.tsx +++ b/packages/staking/src/features/staking/StakingView.tsx @@ -44,7 +44,7 @@ export const StakingView = () => { {(activePage) => ( - + {activePage === Page.overview && } {activePage === Page.browsePools && } {activePage === Page.activity && } diff --git a/packages/staking/src/features/theme/colors.ts b/packages/staking/src/features/theme/colors.ts index 555f53a090..da605a277b 100644 --- a/packages/staking/src/features/theme/colors.ts +++ b/packages/staking/src/features/theme/colors.ts @@ -1,6 +1,7 @@ import { darkColorScheme, laceGradient, lightColorScheme } from '@lace/ui'; export const colorsContract = { + $activityNoActivityTextColor: '', $bannerBellIconColor: '', $bannerInfoIconColor: '', $delegationCardInfoLabelColor: '', @@ -29,6 +30,7 @@ export const colorsContract = { }; export const lightThemeColors: typeof colorsContract = { + $activityNoActivityTextColor: lightColorScheme.$primary_dark_grey, $bannerBellIconColor: lightColorScheme.$primary_accent_purple, $bannerInfoIconColor: lightColorScheme.$primary_accent_purple, $delegationCardInfoLabelColor: lightColorScheme.$primary_dark_grey, @@ -57,6 +59,7 @@ export const lightThemeColors: typeof colorsContract = { }; export const darkThemeColors: typeof colorsContract = { + $activityNoActivityTextColor: darkColorScheme.$primary_light_grey, $bannerBellIconColor: darkColorScheme.$primary_accent_purple, $bannerInfoIconColor: darkColorScheme.$primary_accent_purple, $delegationCardInfoLabelColor: darkColorScheme.$primary_light_grey, From 665fed5ee73dc87c9397bce661d73ac194336672 Mon Sep 17 00:00:00 2001 From: Tomek Marciniak Date: Thu, 9 Nov 2023 10:49:28 +0100 Subject: [PATCH 03/13] feat(staking): add past epochs rewards chart --- packages/staking/package.json | 3 + .../src/features/activity/Activity.tsx | 2 + .../src/features/activity/EpochsSwitch.tsx | 20 +++++ .../features/activity/PastEpochsRewards.tsx | 23 ++++++ .../src/features/activity/PoolIndicator.tsx | 7 ++ .../activity/RewardsChart.module copy.scss | 3 + .../activity/RewardsChart.module.scss | 3 + .../src/features/activity/RewardsChart.tsx | 42 ++++++++++ .../features/activity/RewardsChartTooltip.tsx | 38 +++++++++ .../src/features/activity/RewardsHistory.tsx | 2 +- .../staking/src/features/activity/const.ts | 16 ++++ .../activity/usePoolInPortfolioPresence.tsx | 10 +++ .../features/activity/useRewardsByEpoch.ts | 77 +++++++++++++++++++ .../src/features/i18n/translations/en.ts | 5 ++ packages/staking/src/features/i18n/types.ts | 7 ++ yarn.lock | 49 +++++++++++- 16 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 packages/staking/src/features/activity/EpochsSwitch.tsx create mode 100644 packages/staking/src/features/activity/PastEpochsRewards.tsx create mode 100644 packages/staking/src/features/activity/PoolIndicator.tsx create mode 100644 packages/staking/src/features/activity/RewardsChart.module copy.scss create mode 100644 packages/staking/src/features/activity/RewardsChart.module.scss create mode 100644 packages/staking/src/features/activity/RewardsChart.tsx create mode 100644 packages/staking/src/features/activity/RewardsChartTooltip.tsx create mode 100644 packages/staking/src/features/activity/const.ts create mode 100644 packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx create mode 100644 packages/staking/src/features/activity/useRewardsByEpoch.ts diff --git a/packages/staking/package.json b/packages/staking/package.json index 4d44c00627..bc2f8734e9 100644 --- a/packages/staking/package.json +++ b/packages/staking/package.json @@ -59,11 +59,14 @@ "i18next": "^22.5.1", "immer": "^10.0.2", "lodash": "4.17.21", + "rambda": "^8.5.0", "react-copy-to-clipboard": "^5.1.0", "react-i18next": "^12.3.1", + "recharts": "^2.9.2", "zustand": "^4.4.1" }, "devDependencies": { + "@cardano-sdk/core": "0.21.0", "@cardano-sdk/input-selection": "0.12.4", "@cardano-sdk/tx-construction": "0.14.2", "@cardano-sdk/util": "0.14.2", diff --git a/packages/staking/src/features/activity/Activity.tsx b/packages/staking/src/features/activity/Activity.tsx index f6fb246cc4..e35ba53cd9 100644 --- a/packages/staking/src/features/activity/Activity.tsx +++ b/packages/staking/src/features/activity/Activity.tsx @@ -1,6 +1,7 @@ import { StateStatus, useOutsideHandles } from 'features/outside-handles-provider'; import { getGroupedRewardsActivities } from './helpers/getGroupedRewardsHistory'; import { NoStakingActivity } from './NoStakingActivity'; +import { PastEpochsRewards } from './PastEpochsRewards'; import { RewardsHistory } from './RewardsHistory'; export const Activity = () => { @@ -10,6 +11,7 @@ export const Activity = () => { return ( <> + {walletActivitiesStatus === StateStatus.LOADED && groupedRewardsActivities.length === 0 ? ( ) : ( diff --git a/packages/staking/src/features/activity/EpochsSwitch.tsx b/packages/staking/src/features/activity/EpochsSwitch.tsx new file mode 100644 index 0000000000..f9631ba59f --- /dev/null +++ b/packages/staking/src/features/activity/EpochsSwitch.tsx @@ -0,0 +1,20 @@ +import { ControlButton, Flex, Text } from '@lace/ui'; + +// eslint-disable-next-line no-magic-numbers +const EPOCHS_OPTIONS = [5, 15]; + +export type EpochsSwitchProps = { + epochsCount: number; + setEpochsCount: (epochsCount: number) => void; +}; + +export const EpochsSwitch = ({ epochsCount, setEpochsCount }: EpochsSwitchProps) => ( + + Epochs: + {EPOCHS_OPTIONS.map((option, i) => { + const activeOption = epochsCount === option; + const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined; + return setEpochsCount(option)} />; + })} + +); diff --git a/packages/staking/src/features/activity/PastEpochsRewards.tsx b/packages/staking/src/features/activity/PastEpochsRewards.tsx new file mode 100644 index 0000000000..ddf087cfca --- /dev/null +++ b/packages/staking/src/features/activity/PastEpochsRewards.tsx @@ -0,0 +1,23 @@ +import { Flex, Text } from '@lace/ui'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { EpochsSwitch } from './EpochsSwitch'; +import { RewardsChart } from './RewardsChart'; +import { useRewardsByEpoch } from './useRewardsByEpoch'; + +const DEFAULT_LAST_EPOCHS = 5; + +export const PastEpochsRewards = () => { + const [epochsCount, setEpochsCount] = useState(DEFAULT_LAST_EPOCHS); + const { t } = useTranslation(); + const { rewardsByEpoch } = useRewardsByEpoch({ epochsCount }); + return ( + <> + + {t('activity.rewardsChart.title')} + + + {rewardsByEpoch && } + + ); +}; diff --git a/packages/staking/src/features/activity/PoolIndicator.tsx b/packages/staking/src/features/activity/PoolIndicator.tsx new file mode 100644 index 0000000000..00dee66204 --- /dev/null +++ b/packages/staking/src/features/activity/PoolIndicator.tsx @@ -0,0 +1,7 @@ +import { PIE_CHART_DEFAULT_COLOR_SET } from '@lace/ui'; + +export const PoolIndicator = ({ color = PIE_CHART_DEFAULT_COLOR_SET[0] }: { color?: string }) => ( + + + +); diff --git a/packages/staking/src/features/activity/RewardsChart.module copy.scss b/packages/staking/src/features/activity/RewardsChart.module copy.scss new file mode 100644 index 0000000000..c399927c34 --- /dev/null +++ b/packages/staking/src/features/activity/RewardsChart.module copy.scss @@ -0,0 +1,3 @@ +.chartContainer { + height: auto; +} diff --git a/packages/staking/src/features/activity/RewardsChart.module.scss b/packages/staking/src/features/activity/RewardsChart.module.scss new file mode 100644 index 0000000000..c399927c34 --- /dev/null +++ b/packages/staking/src/features/activity/RewardsChart.module.scss @@ -0,0 +1,3 @@ +.chartContainer { + height: auto; +} diff --git a/packages/staking/src/features/activity/RewardsChart.tsx b/packages/staking/src/features/activity/RewardsChart.tsx new file mode 100644 index 0000000000..7513105322 --- /dev/null +++ b/packages/staking/src/features/activity/RewardsChart.tsx @@ -0,0 +1,42 @@ +import { Card, PIE_CHART_DEFAULT_COLOR_SET } from '@lace/ui'; +import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import { GRAYSCALE_PALETTE, maxPoolsIterator } from './const'; +import { RewardsChartTooltip } from './RewardsChartTooltip'; +import { usePoolInPortfolioPresence } from './usePoolInPortfolioPresence'; +import { RewardsByEpoch } from './useRewardsByEpoch'; + +export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { + const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence(); + return ( + + + + + `${value} ADA`} /> + } /> + {maxPoolsIterator.map((_, i) => ( + + {chartData.map((entry, j) => { + const fill = + entry.rewards[i]?.poolId && checkIfPoolIsInPortfolio(entry.rewards[i]?.poolId) + ? PIE_CHART_DEFAULT_COLOR_SET[i] + : GRAYSCALE_PALETTE[i]; + return ; + })} + + ))} + + + + ); +}; diff --git a/packages/staking/src/features/activity/RewardsChartTooltip.tsx b/packages/staking/src/features/activity/RewardsChartTooltip.tsx new file mode 100644 index 0000000000..9eb13cb26f --- /dev/null +++ b/packages/staking/src/features/activity/RewardsChartTooltip.tsx @@ -0,0 +1,38 @@ +import { Card, Flex, PIE_CHART_DEFAULT_COLOR_SET, Text } from '@lace/ui'; +import { TooltipProps } from 'recharts'; +import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import { GRAYSCALE_PALETTE, maxPoolsIterator } from './const'; +import { PoolIndicator } from './PoolIndicator'; +import { usePoolInPortfolioPresence } from './usePoolInPortfolioPresence'; + +export const RewardsChartTooltip = ({ active, payload, label }: TooltipProps) => { + const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence(); + if (active && payload && payload.length > 0) { + return ( + + + Epoch {label} + + {maxPoolsIterator.map((_, i) => { + const poolId = payload[i]?.payload?.rewards?.[i]?.poolId; + const poolInPortfolio = payload[i] && checkIfPoolIsInPortfolio(poolId); + return ( + payload[i] && ( + + + + {payload[i]?.payload?.rewards?.[i]?.metadata.name} + Rewards: {payload[i]?.value} ADA + + + ) + ); + })} + + + + ); + } + + return null; +}; diff --git a/packages/staking/src/features/activity/RewardsHistory.tsx b/packages/staking/src/features/activity/RewardsHistory.tsx index 4f2a2cc45f..d17c010722 100644 --- a/packages/staking/src/features/activity/RewardsHistory.tsx +++ b/packages/staking/src/features/activity/RewardsHistory.tsx @@ -15,7 +15,7 @@ export const RewardsHistory = ({ groupedRewardsActivities, walletActivitiesStatu return ( <> - + {t('activity.rewardsHistory.title')} diff --git a/packages/staking/src/features/activity/const.ts b/packages/staking/src/features/activity/const.ts new file mode 100644 index 0000000000..7e5f742ac2 --- /dev/null +++ b/packages/staking/src/features/activity/const.ts @@ -0,0 +1,16 @@ +import { MAX_POOLS_COUNT } from 'features/store'; + +export const maxPoolsIterator = Array.from({ length: MAX_POOLS_COUNT }); + +export const GRAYSCALE_PALETTE = [ + '#343434', + '#4a4a4a', + '#616161', + '#787878', + '#8e8e8e', + '#a5a5a5', + '#bbbbbb', + '#d2d2d2', + '#e8e8e8', + '#fafafa', +]; diff --git a/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx b/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx new file mode 100644 index 0000000000..142172ea73 --- /dev/null +++ b/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx @@ -0,0 +1,10 @@ +import { Cardano } from '@cardano-sdk/core'; +import { useDelegationPortfolioStore } from 'features/store'; + +// Didn't use the useDelegationPortfolioStore helper for that, because it requires hexId. +export const usePoolInPortfolioPresence = () => { + const currentPortfolio = useDelegationPortfolioStore((store) => store.currentPortfolio); + const checkIfPoolIsInPortfolio = (poolId?: Cardano.PoolId) => + currentPortfolio.some((portfolioEntry) => portfolioEntry.stakePool.id === poolId); + return { checkIfPoolIsInPortfolio }; +}; diff --git a/packages/staking/src/features/activity/useRewardsByEpoch.ts b/packages/staking/src/features/activity/useRewardsByEpoch.ts new file mode 100644 index 0000000000..f6948697cb --- /dev/null +++ b/packages/staking/src/features/activity/useRewardsByEpoch.ts @@ -0,0 +1,77 @@ +import { Cardano, Reward } from '@cardano-sdk/core'; +import { RewardsHistory } from '@cardano-sdk/wallet'; +import { Wallet } from '@lace/cardano'; +import { useObservable } from '@lace/common'; +import { useOutsideHandles } from 'features/outside-handles-provider'; +import { groupBy, sortBy, takeLast, uniqBy } from 'rambda'; +import { useEffect, useState } from 'react'; + +export type RewardWithMetadata = Reward & { + poolMetadata: Cardano.StakePoolMetadata | undefined; +}; + +export type RewardsByEpoch = { epoch: number; rewards: RewardWithMetadata[] }[]; + +export type UseRewardsByEpochProps = { + epochsCount: number; +}; + +const getPoolInfos = async (poolIds: Wallet.Cardano.PoolId[], stakePoolProvider: Wallet.StakePoolProvider) => { + const filters: Wallet.QueryStakePoolsArgs = { + filters: { + identifier: { + _condition: 'or', + values: poolIds.map((poolId) => ({ id: poolId })), + }, + }, + pagination: { + limit: 100, + startAt: 0, + }, + }; + const { pageResults: pools } = await stakePoolProvider.queryStakePools(filters); + + return pools; +}; + +type GetRewardsByEpochProps = { + rewardsHistory: RewardsHistory; + stakePoolProvider: Wallet.StakePoolProvider; + epochsCount: number; +}; + +const getRewardsByEpoch = async ({ rewardsHistory, stakePoolProvider, epochsCount }: GetRewardsByEpochProps) => { + const uniqPoolIds = uniqBy((rewards) => rewards.poolId, rewardsHistory.all).map((reward) => reward.poolId); + const stakePoolsData = await getPoolInfos(uniqPoolIds as never, stakePoolProvider); + const rewardsHistoryWithMetadata = rewardsHistory.all.map((reward) => ({ + ...reward, + metadata: stakePoolsData.find((poolInfo) => poolInfo.id === reward.poolId)?.metadata, + rewards: Wallet.util.lovelacesToAdaString(reward.rewards.toString()), + })); + const groupedRewards = groupBy((reward) => reward.epoch as never, rewardsHistoryWithMetadata); + const groupedRewardsArray = Object.entries(groupedRewards).map(([epoch, rewards]) => ({ + epoch: Number.parseInt(epoch), + rewards, + })); + const sortedByEpoch = sortBy((entry) => entry.epoch, groupedRewardsArray); + return takeLast(epochsCount, sortedByEpoch); +}; + +export const useRewardsByEpoch = ({ epochsCount }: UseRewardsByEpochProps) => { + const [rewardsByEpoch, setRewardsByEpoch] = useState(); + const { walletStoreInMemoryWallet: inMemoryWallet, walletStoreBlockchainProvider } = useOutsideHandles(); + const rewardsHistory = useObservable(inMemoryWallet.delegation.rewardsHistory$); + useEffect(() => { + if (!rewardsHistory) return; + const fetchRewardsByEpoch = async () => { + const result = await getRewardsByEpoch({ + epochsCount, + rewardsHistory, + stakePoolProvider: walletStoreBlockchainProvider.stakePoolProvider, + }); + setRewardsByEpoch(result as never); + }; + void fetchRewardsByEpoch(); + }, [epochsCount, rewardsHistory]); + return { rewardsByEpoch }; +}; diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index 4af359ddb3..f5a02e0bd6 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -1,6 +1,11 @@ import { Translations } from '../types'; export const en: Translations = { + 'activity.rewardsChart.all': 'All', + 'activity.rewardsChart.epochs': 'Epochs', + 'activity.rewardsChart.last': 'Last', + 'activity.rewardsChart.rewards': 'Rewards', + 'activity.rewardsChart.title': 'Rewards', 'activity.rewardsHistory.noStakingActivityYet': 'No staking activity yet.', 'activity.rewardsHistory.title': 'History', 'browsePools.stakePoolTableBrowser.addPool': 'Add pool', diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index 31fc5b4e40..f061c53de4 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -20,6 +20,13 @@ type KeysStructure = { title: ''; noStakingActivityYet: ''; }; + rewardsChart: { + title: ''; + epochs: ''; + last: ''; + all: ''; + rewards: ''; + }; }; browsePools: { stakePoolTableBrowser: { diff --git a/yarn.lock b/yarn.lock index 89c685363e..4f105d7212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5404,7 +5404,7 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/core@npm:0.21.0, @cardano-sdk/core@npm:~0.21.0": +"@cardano-sdk/core@npm:0.21.0, @cardano-sdk/core@npm:^0.21.0, @cardano-sdk/core@npm:~0.21.0": version: 0.21.0 resolution: "@cardano-sdk/core@npm:0.21.0" dependencies: @@ -8026,6 +8026,7 @@ __metadata: resolution: "@lace/staking@workspace:packages/staking" dependencies: "@ant-design/icons": ^4.7.0 + "@cardano-sdk/core": ^0.21.0 "@cardano-sdk/input-selection": 0.12.4 "@cardano-sdk/tx-construction": 0.14.2 "@cardano-sdk/util": 0.14.2 @@ -8053,11 +8054,13 @@ __metadata: immer: ^10.0.2 lodash: 4.17.21 normalize.css: ^8.0.1 + rambda: ^8.5.0 react: 17.0.2 react-copy-to-clipboard: ^5.1.0 react-dom: 17.0.2 react-i18next: ^12.3.1 react-infinite-scroll-component: ^6.1.0 + recharts: ^2.9.2 tsup: ^6.7.0 typescript: ^4.9.5 vite-plugin-checker: ^0.6.0 @@ -38575,6 +38578,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^8.5.0": + version: 8.5.0 + resolution: "rambda@npm:8.5.0" + checksum: 4f188195f9859e5b0955f5eddfa454855d3d390c8dfdf22f19b6e493ad978ba1123b09201ef0ebddc2ab608ef22b68c541582a1810cbf3f789870dd10bba5598 + languageName: node + linkType: hard + "ramda@npm:^0.21.0": version: 0.21.0 resolution: "ramda@npm:0.21.0" @@ -40065,6 +40075,20 @@ __metadata: languageName: node linkType: hard +"react-smooth@npm:^2.0.4": + version: 2.0.5 + resolution: "react-smooth@npm:2.0.5" + dependencies: + fast-equals: ^5.0.0 + react-transition-group: 2.9.0 + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 914c17f741e8b533ff6e3d5e3285aea0625cdd0f98e04202d01351f9516dbdc0a0e297dc22cc2377d6916fb819da8d4ed999c0314a4c186592ca51870012e6f7 + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -40447,6 +40471,27 @@ __metadata: languageName: node linkType: hard +"recharts@npm:^2.9.2": + version: 2.9.2 + resolution: "recharts@npm:2.9.2" + dependencies: + classnames: ^2.2.5 + eventemitter3: ^4.0.1 + lodash: ^4.17.19 + react-is: ^16.10.2 + react-resize-detector: ^8.0.4 + react-smooth: ^2.0.4 + recharts-scale: ^0.4.4 + tiny-invariant: ^1.3.1 + victory-vendor: ^36.6.8 + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 77a87e3d91229ac5400240409568e3345ded50fc117e70e43e61a135b44bbc3164048704393aa988201ea2989278e1c4e96ce350f6a2f87044d1f0a48f290e84 + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -44537,7 +44582,7 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.1.0": +"tiny-invariant@npm:^1.1.0, tiny-invariant@npm:^1.3.1": version: 1.3.1 resolution: "tiny-invariant@npm:1.3.1" checksum: 872dbd1ff20a21303a2fd20ce3a15602cfa7fcf9b228bd694a52e2938224313b5385a1078cb667ed7375d1612194feaca81c4ecbe93121ca1baebe344de4f84c From 801f33d91a25adb09d4d532253b81d7fe8f60206 Mon Sep 17 00:00:00 2001 From: Tomek Marciniak Date: Thu, 9 Nov 2023 11:04:47 +0100 Subject: [PATCH 04/13] fix(staking): fix yarn lock resolution --- .../features/activity/EpochsSwitch.module.scss | 4 ++++ .../src/features/activity/EpochsSwitch.tsx | 15 +++++++++------ yarn.lock | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 packages/staking/src/features/activity/EpochsSwitch.module.scss diff --git a/packages/staking/src/features/activity/EpochsSwitch.module.scss b/packages/staking/src/features/activity/EpochsSwitch.module.scss new file mode 100644 index 0000000000..de49cf4684 --- /dev/null +++ b/packages/staking/src/features/activity/EpochsSwitch.module.scss @@ -0,0 +1,4 @@ +.buttonsBackground { + background-color: var(--light-mode-light-grey, var(--dark-mode-dark-grey, #2f2f2f)); + border-radius: 1rem; +} diff --git a/packages/staking/src/features/activity/EpochsSwitch.tsx b/packages/staking/src/features/activity/EpochsSwitch.tsx index f9631ba59f..ae6a44d6f7 100644 --- a/packages/staking/src/features/activity/EpochsSwitch.tsx +++ b/packages/staking/src/features/activity/EpochsSwitch.tsx @@ -1,4 +1,5 @@ import { ControlButton, Flex, Text } from '@lace/ui'; +import styles from './EpochsSwitch.module.scss'; // eslint-disable-next-line no-magic-numbers const EPOCHS_OPTIONS = [5, 15]; @@ -9,12 +10,14 @@ export type EpochsSwitchProps = { }; export const EpochsSwitch = ({ epochsCount, setEpochsCount }: EpochsSwitchProps) => ( - + Epochs: - {EPOCHS_OPTIONS.map((option, i) => { - const activeOption = epochsCount === option; - const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined; - return setEpochsCount(option)} />; - })} + + {EPOCHS_OPTIONS.map((option, i) => { + const activeOption = epochsCount === option; + const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined; + return setEpochsCount(option)} />; + })} + ); diff --git a/yarn.lock b/yarn.lock index 4f105d7212..e64d139766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5404,7 +5404,7 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/core@npm:0.21.0, @cardano-sdk/core@npm:^0.21.0, @cardano-sdk/core@npm:~0.21.0": +"@cardano-sdk/core@npm:0.21.0, @cardano-sdk/core@npm:~0.21.0": version: 0.21.0 resolution: "@cardano-sdk/core@npm:0.21.0" dependencies: @@ -8026,7 +8026,7 @@ __metadata: resolution: "@lace/staking@workspace:packages/staking" dependencies: "@ant-design/icons": ^4.7.0 - "@cardano-sdk/core": ^0.21.0 + "@cardano-sdk/core": 0.21.0 "@cardano-sdk/input-selection": 0.12.4 "@cardano-sdk/tx-construction": 0.14.2 "@cardano-sdk/util": 0.14.2 From be0a21c107ab095fd68e307437cca25e79a1773f Mon Sep 17 00:00:00 2001 From: Tomek Marciniak Date: Thu, 9 Nov 2023 13:24:54 +0100 Subject: [PATCH 05/13] fix(staking): add missing translations --- .../src/features/activity/EpochsSwitch.tsx | 34 ++++++++++++------- .../activity/RewardsChart.module copy.scss | 3 -- .../activity/RewardsChart.module.scss | 3 -- .../features/activity/RewardsChartTooltip.tsx | 10 ++++-- .../src/features/i18n/translations/en.ts | 1 + packages/staking/src/features/i18n/types.ts | 1 + 6 files changed, 32 insertions(+), 20 deletions(-) delete mode 100644 packages/staking/src/features/activity/RewardsChart.module copy.scss delete mode 100644 packages/staking/src/features/activity/RewardsChart.module.scss diff --git a/packages/staking/src/features/activity/EpochsSwitch.tsx b/packages/staking/src/features/activity/EpochsSwitch.tsx index ae6a44d6f7..d72cb2b819 100644 --- a/packages/staking/src/features/activity/EpochsSwitch.tsx +++ b/packages/staking/src/features/activity/EpochsSwitch.tsx @@ -1,23 +1,33 @@ import { ControlButton, Flex, Text } from '@lace/ui'; +import { useTranslation } from 'react-i18next'; import styles from './EpochsSwitch.module.scss'; // eslint-disable-next-line no-magic-numbers const EPOCHS_OPTIONS = [5, 15]; -export type EpochsSwitchProps = { +type EpochsSwitchProps = { epochsCount: number; setEpochsCount: (epochsCount: number) => void; }; -export const EpochsSwitch = ({ epochsCount, setEpochsCount }: EpochsSwitchProps) => ( - - Epochs: - - {EPOCHS_OPTIONS.map((option, i) => { - const activeOption = epochsCount === option; - const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined; - return setEpochsCount(option)} />; - })} +export const EpochsSwitch = ({ epochsCount, setEpochsCount }: EpochsSwitchProps) => { + const { t } = useTranslation(); + return ( + + {t('activity.rewardsChart.epochs')}: + + {EPOCHS_OPTIONS.map((option, i) => { + const activeOption = epochsCount === option; + const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined; + return ( + setEpochsCount(option)} + /> + ); + })} + - -); + ); +}; diff --git a/packages/staking/src/features/activity/RewardsChart.module copy.scss b/packages/staking/src/features/activity/RewardsChart.module copy.scss deleted file mode 100644 index c399927c34..0000000000 --- a/packages/staking/src/features/activity/RewardsChart.module copy.scss +++ /dev/null @@ -1,3 +0,0 @@ -.chartContainer { - height: auto; -} diff --git a/packages/staking/src/features/activity/RewardsChart.module.scss b/packages/staking/src/features/activity/RewardsChart.module.scss deleted file mode 100644 index c399927c34..0000000000 --- a/packages/staking/src/features/activity/RewardsChart.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.chartContainer { - height: auto; -} diff --git a/packages/staking/src/features/activity/RewardsChartTooltip.tsx b/packages/staking/src/features/activity/RewardsChartTooltip.tsx index 9eb13cb26f..9d4f76a1c5 100644 --- a/packages/staking/src/features/activity/RewardsChartTooltip.tsx +++ b/packages/staking/src/features/activity/RewardsChartTooltip.tsx @@ -1,4 +1,5 @@ import { Card, Flex, PIE_CHART_DEFAULT_COLOR_SET, Text } from '@lace/ui'; +import { useTranslation } from 'react-i18next'; import { TooltipProps } from 'recharts'; import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; import { GRAYSCALE_PALETTE, maxPoolsIterator } from './const'; @@ -6,12 +7,15 @@ import { PoolIndicator } from './PoolIndicator'; import { usePoolInPortfolioPresence } from './usePoolInPortfolioPresence'; export const RewardsChartTooltip = ({ active, payload, label }: TooltipProps) => { + const { t } = useTranslation(); const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence(); if (active && payload && payload.length > 0) { return ( - Epoch {label} + + {t('activity.rewardsChart.epoch')} {label} + {maxPoolsIterator.map((_, i) => { const poolId = payload[i]?.payload?.rewards?.[i]?.poolId; @@ -22,7 +26,9 @@ export const RewardsChartTooltip = ({ active, payload, label }: TooltipProps {payload[i]?.payload?.rewards?.[i]?.metadata.name} - Rewards: {payload[i]?.value} ADA + + {t('activity.rewardsChart.rewards')}: {payload[i]?.value} ADA + ) diff --git a/packages/staking/src/features/i18n/translations/en.ts b/packages/staking/src/features/i18n/translations/en.ts index f5a02e0bd6..4d9ecfc7ab 100644 --- a/packages/staking/src/features/i18n/translations/en.ts +++ b/packages/staking/src/features/i18n/translations/en.ts @@ -2,6 +2,7 @@ import { Translations } from '../types'; export const en: Translations = { 'activity.rewardsChart.all': 'All', + 'activity.rewardsChart.epoch': 'Epoch', 'activity.rewardsChart.epochs': 'Epochs', 'activity.rewardsChart.last': 'Last', 'activity.rewardsChart.rewards': 'Rewards', diff --git a/packages/staking/src/features/i18n/types.ts b/packages/staking/src/features/i18n/types.ts index f061c53de4..8b4452399c 100644 --- a/packages/staking/src/features/i18n/types.ts +++ b/packages/staking/src/features/i18n/types.ts @@ -23,6 +23,7 @@ type KeysStructure = { rewardsChart: { title: ''; epochs: ''; + epoch: ''; last: ''; all: ''; rewards: ''; From 436057ca15ff31c0c47d8a4ed84d9e349311b158 Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 14:53:35 +0100 Subject: [PATCH 06/13] fix(staking): fix types --- .../store/delegationPortfolioStore/stateMachine/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts index 41b5639f85..84a6e3ef8a 100644 --- a/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts +++ b/packages/staking/src/features/store/delegationPortfolioStore/stateMachine/types.ts @@ -89,6 +89,9 @@ export type StateOverview = MakeState<{ export type StateActivity = MakeState<{ activeDelegationFlow: DelegationFlow.Activity; activeDrawerStep: undefined; + draftPortfolio: undefined; + pendingSelectedPortfolio: undefined; + viewedStakePool: undefined; }>; export type StateCurrentPoolDetails = MakeState<{ From f09ac555ea1b1a630c8bd34cd379a90dd263ef97 Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 14:57:24 +0100 Subject: [PATCH 07/13] fix(staking): show rewards chart only if non-empty --- packages/staking/src/features/activity/Activity.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/staking/src/features/activity/Activity.tsx b/packages/staking/src/features/activity/Activity.tsx index e35ba53cd9..80801527d8 100644 --- a/packages/staking/src/features/activity/Activity.tsx +++ b/packages/staking/src/features/activity/Activity.tsx @@ -11,14 +11,16 @@ export const Activity = () => { return ( <> - {walletActivitiesStatus === StateStatus.LOADED && groupedRewardsActivities.length === 0 ? ( ) : ( - + <> + + + )} ); From f417372e644a74fdb41e3f5603e3c5570ffcaeb7 Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 16:26:23 +0100 Subject: [PATCH 08/13] refactor(staking): refactor useRewardsByEpoch --- .../features/activity/useRewardsByEpoch.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/staking/src/features/activity/useRewardsByEpoch.ts b/packages/staking/src/features/activity/useRewardsByEpoch.ts index f6948697cb..3e89df1c0a 100644 --- a/packages/staking/src/features/activity/useRewardsByEpoch.ts +++ b/packages/staking/src/features/activity/useRewardsByEpoch.ts @@ -6,8 +6,9 @@ import { useOutsideHandles } from 'features/outside-handles-provider'; import { groupBy, sortBy, takeLast, uniqBy } from 'rambda'; import { useEffect, useState } from 'react'; -export type RewardWithMetadata = Reward & { - poolMetadata: Cardano.StakePoolMetadata | undefined; +export type RewardWithMetadata = Omit & { + metadata: Cardano.StakePoolMetadata | undefined; + rewards: string; // TODO move the transformation to the chart }; export type RewardsByEpoch = { epoch: number; rewards: RewardWithMetadata[] }[]; @@ -40,15 +41,17 @@ type GetRewardsByEpochProps = { epochsCount: number; }; -const getRewardsByEpoch = async ({ rewardsHistory, stakePoolProvider, epochsCount }: GetRewardsByEpochProps) => { - const uniqPoolIds = uniqBy((rewards) => rewards.poolId, rewardsHistory.all).map((reward) => reward.poolId); - const stakePoolsData = await getPoolInfos(uniqPoolIds as never, stakePoolProvider); +const buildRewardsByEpoch = async ({ rewardsHistory, stakePoolProvider, epochsCount }: GetRewardsByEpochProps) => { + const uniqPoolIds = uniqBy((rewards) => rewards.poolId, rewardsHistory.all) + .map((reward) => reward.poolId) + .filter(Boolean) as Wallet.Cardano.PoolId[]; + const stakePoolsData = await getPoolInfos(uniqPoolIds, stakePoolProvider); const rewardsHistoryWithMetadata = rewardsHistory.all.map((reward) => ({ ...reward, metadata: stakePoolsData.find((poolInfo) => poolInfo.id === reward.poolId)?.metadata, rewards: Wallet.util.lovelacesToAdaString(reward.rewards.toString()), })); - const groupedRewards = groupBy((reward) => reward.epoch as never, rewardsHistoryWithMetadata); + const groupedRewards = groupBy(({ epoch }) => epoch.toString(), rewardsHistoryWithMetadata); const groupedRewardsArray = Object.entries(groupedRewards).map(([epoch, rewards]) => ({ epoch: Number.parseInt(epoch), rewards, @@ -63,15 +66,14 @@ export const useRewardsByEpoch = ({ epochsCount }: UseRewardsByEpochProps) => { const rewardsHistory = useObservable(inMemoryWallet.delegation.rewardsHistory$); useEffect(() => { if (!rewardsHistory) return; - const fetchRewardsByEpoch = async () => { - const result = await getRewardsByEpoch({ + (async () => { + const result = await buildRewardsByEpoch({ epochsCount, rewardsHistory, stakePoolProvider: walletStoreBlockchainProvider.stakePoolProvider, }); - setRewardsByEpoch(result as never); - }; - void fetchRewardsByEpoch(); - }, [epochsCount, rewardsHistory]); + setRewardsByEpoch(result); + })(); + }, [epochsCount, rewardsHistory, walletStoreBlockchainProvider]); return { rewardsByEpoch }; }; From 60827b2d093711f5f4bce4bd5af13db5f86cf13b Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 17:01:10 +0100 Subject: [PATCH 09/13] fix(staking): fix epoch numbers in rewards chart --- .../staking/src/features/activity/RewardsChart.tsx | 2 +- .../src/features/activity/useRewardsByEpoch.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/staking/src/features/activity/RewardsChart.tsx b/packages/staking/src/features/activity/RewardsChart.tsx index 7513105322..b677271530 100644 --- a/packages/staking/src/features/activity/RewardsChart.tsx +++ b/packages/staking/src/features/activity/RewardsChart.tsx @@ -21,7 +21,7 @@ export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { top: 32, }} > - + `${value} ADA`} /> } /> {maxPoolsIterator.map((_, i) => ( diff --git a/packages/staking/src/features/activity/useRewardsByEpoch.ts b/packages/staking/src/features/activity/useRewardsByEpoch.ts index 3e89df1c0a..7bf904d805 100644 --- a/packages/staking/src/features/activity/useRewardsByEpoch.ts +++ b/packages/staking/src/features/activity/useRewardsByEpoch.ts @@ -6,12 +6,13 @@ import { useOutsideHandles } from 'features/outside-handles-provider'; import { groupBy, sortBy, takeLast, uniqBy } from 'rambda'; import { useEffect, useState } from 'react'; -export type RewardWithMetadata = Omit & { +export type RewardWithMetadata = Omit & { metadata: Cardano.StakePoolMetadata | undefined; + spendableEpoch: Cardano.EpochNo; rewards: string; // TODO move the transformation to the chart }; -export type RewardsByEpoch = { epoch: number; rewards: RewardWithMetadata[] }[]; +export type RewardsByEpoch = { spendableEpoch: Cardano.EpochNo; rewards: RewardWithMetadata[] }[]; export type UseRewardsByEpochProps = { epochsCount: number; @@ -42,6 +43,7 @@ type GetRewardsByEpochProps = { }; const buildRewardsByEpoch = async ({ rewardsHistory, stakePoolProvider, epochsCount }: GetRewardsByEpochProps) => { + const REWARD_SPENDABLE_DELAY_EPOCHS = 2; const uniqPoolIds = uniqBy((rewards) => rewards.poolId, rewardsHistory.all) .map((reward) => reward.poolId) .filter(Boolean) as Wallet.Cardano.PoolId[]; @@ -50,13 +52,14 @@ const buildRewardsByEpoch = async ({ rewardsHistory, stakePoolProvider, epochsCo ...reward, metadata: stakePoolsData.find((poolInfo) => poolInfo.id === reward.poolId)?.metadata, rewards: Wallet.util.lovelacesToAdaString(reward.rewards.toString()), + spendableEpoch: (reward.epoch + REWARD_SPENDABLE_DELAY_EPOCHS) as Cardano.EpochNo, })); const groupedRewards = groupBy(({ epoch }) => epoch.toString(), rewardsHistoryWithMetadata); const groupedRewardsArray = Object.entries(groupedRewards).map(([epoch, rewards]) => ({ - epoch: Number.parseInt(epoch), rewards, + spendableEpoch: (Number.parseInt(epoch) + REWARD_SPENDABLE_DELAY_EPOCHS) as Cardano.EpochNo, })); - const sortedByEpoch = sortBy((entry) => entry.epoch, groupedRewardsArray); + const sortedByEpoch = sortBy((entry) => entry.spendableEpoch, groupedRewardsArray); return takeLast(epochsCount, sortedByEpoch); }; From 843637e3d592386b26f3e6a0705b5060ce472593 Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 18:21:34 +0100 Subject: [PATCH 10/13] fix(staking): use consistent coloring of pools across epochs --- .../src/features/activity/RewardsChart.tsx | 21 ++++----- .../features/activity/RewardsChartTooltip.tsx | 37 ++++++++------- .../staking/src/features/activity/const.ts | 16 ------- .../activity/usePoolInPortfolioPresence.tsx | 10 ---- .../useRewardsChartPoolsColorMapper.tsx | 47 +++++++++++++++++++ 5 files changed, 76 insertions(+), 55 deletions(-) delete mode 100644 packages/staking/src/features/activity/const.ts delete mode 100644 packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx create mode 100644 packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx diff --git a/packages/staking/src/features/activity/RewardsChart.tsx b/packages/staking/src/features/activity/RewardsChart.tsx index b677271530..5829a21b10 100644 --- a/packages/staking/src/features/activity/RewardsChart.tsx +++ b/packages/staking/src/features/activity/RewardsChart.tsx @@ -1,12 +1,14 @@ -import { Card, PIE_CHART_DEFAULT_COLOR_SET } from '@lace/ui'; +import { Card } from '@lace/ui'; import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import { GRAYSCALE_PALETTE, maxPoolsIterator } from './const'; +import type { RewardsByEpoch } from './useRewardsByEpoch'; import { RewardsChartTooltip } from './RewardsChartTooltip'; -import { usePoolInPortfolioPresence } from './usePoolInPortfolioPresence'; -import { RewardsByEpoch } from './useRewardsByEpoch'; +import { useRewardsChartPoolsColorMapper } from './useRewardsChartPoolsColorMapper'; export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { - const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence(); + const poolColorMapper = useRewardsChartPoolsColorMapper(chartData); + // eslint-disable-next-line unicorn/no-array-reduce + const maxPoolsPerEpochCount = chartData.reduce((acc, epochRewards) => Math.max(acc, epochRewards.rewards.length), 0); + return ( @@ -23,14 +25,11 @@ export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { > `${value} ADA`} /> - } /> - {maxPoolsIterator.map((_, i) => ( + } /> + {Array.from({ length: maxPoolsPerEpochCount }).map((_, i) => ( {chartData.map((entry, j) => { - const fill = - entry.rewards[i]?.poolId && checkIfPoolIsInPortfolio(entry.rewards[i]?.poolId) - ? PIE_CHART_DEFAULT_COLOR_SET[i] - : GRAYSCALE_PALETTE[i]; + const fill = poolColorMapper(entry.rewards[i]?.poolId); return ; })} diff --git a/packages/staking/src/features/activity/RewardsChartTooltip.tsx b/packages/staking/src/features/activity/RewardsChartTooltip.tsx index 9d4f76a1c5..c8f9e95b32 100644 --- a/packages/staking/src/features/activity/RewardsChartTooltip.tsx +++ b/packages/staking/src/features/activity/RewardsChartTooltip.tsx @@ -1,14 +1,18 @@ -import { Card, Flex, PIE_CHART_DEFAULT_COLOR_SET, Text } from '@lace/ui'; +import { Card, Flex, Text } from '@lace/ui'; import { useTranslation } from 'react-i18next'; import { TooltipProps } from 'recharts'; import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; -import { GRAYSCALE_PALETTE, maxPoolsIterator } from './const'; import { PoolIndicator } from './PoolIndicator'; -import { usePoolInPortfolioPresence } from './usePoolInPortfolioPresence'; +import { useRewardsChartPoolsColorMapper } from './useRewardsChartPoolsColorMapper'; -export const RewardsChartTooltip = ({ active, payload, label }: TooltipProps) => { +export const RewardsChartTooltip = ({ + active, + payload, + label, + poolColorMapper, +}: TooltipProps & { poolColorMapper: ReturnType }) => { const { t } = useTranslation(); - const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence(); + if (active && payload && payload.length > 0) { return ( @@ -17,21 +21,18 @@ export const RewardsChartTooltip = ({ active, payload, label }: TooltipProps - {maxPoolsIterator.map((_, i) => { - const poolId = payload[i]?.payload?.rewards?.[i]?.poolId; - const poolInPortfolio = payload[i] && checkIfPoolIsInPortfolio(poolId); + {payload.map((p, i) => { + const poolId = p.payload?.rewards?.[i]?.poolId; return ( - payload[i] && ( - - - - {payload[i]?.payload?.rewards?.[i]?.metadata.name} - - {t('activity.rewardsChart.rewards')}: {payload[i]?.value} ADA - - + + + + {p.payload?.rewards?.[i]?.metadata.name} + + {t('activity.rewardsChart.rewards')}: {payload[i]?.value} ADA + - ) + ); })} diff --git a/packages/staking/src/features/activity/const.ts b/packages/staking/src/features/activity/const.ts deleted file mode 100644 index 7e5f742ac2..0000000000 --- a/packages/staking/src/features/activity/const.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MAX_POOLS_COUNT } from 'features/store'; - -export const maxPoolsIterator = Array.from({ length: MAX_POOLS_COUNT }); - -export const GRAYSCALE_PALETTE = [ - '#343434', - '#4a4a4a', - '#616161', - '#787878', - '#8e8e8e', - '#a5a5a5', - '#bbbbbb', - '#d2d2d2', - '#e8e8e8', - '#fafafa', -]; diff --git a/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx b/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx deleted file mode 100644 index 142172ea73..0000000000 --- a/packages/staking/src/features/activity/usePoolInPortfolioPresence.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Cardano } from '@cardano-sdk/core'; -import { useDelegationPortfolioStore } from 'features/store'; - -// Didn't use the useDelegationPortfolioStore helper for that, because it requires hexId. -export const usePoolInPortfolioPresence = () => { - const currentPortfolio = useDelegationPortfolioStore((store) => store.currentPortfolio); - const checkIfPoolIsInPortfolio = (poolId?: Cardano.PoolId) => - currentPortfolio.some((portfolioEntry) => portfolioEntry.stakePool.id === poolId); - return { checkIfPoolIsInPortfolio }; -}; diff --git a/packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx b/packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx new file mode 100644 index 0000000000..29ffe3d476 --- /dev/null +++ b/packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx @@ -0,0 +1,47 @@ +/* eslint-disable unicorn/no-array-reduce */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { PIE_CHART_DEFAULT_COLOR_SET } from '@lace/ui'; +import { useDelegationPortfolioStore } from 'features/store'; +import difference from 'lodash/difference'; +import { useMemo } from 'react'; +import type { RewardsByEpoch } from './useRewardsByEpoch'; +import type { Cardano } from '@cardano-sdk/core'; + +const GRAYSCALE_PALETTE = [ + '#343434', + '#4a4a4a', + '#616161', + '#787878', + '#8e8e8e', + '#a5a5a5', + '#bbbbbb', + '#d2d2d2', + '#e8e8e8', + '#fafafa', +]; + +export const useRewardsChartPoolsColorMapper = (rewardsByEpoch: RewardsByEpoch) => { + const { currentPortfolio } = useDelegationPortfolioStore(); + + const coloring = useMemo(() => { + const poolsInPortfolio = currentPortfolio.map(({ stakePool }) => stakePool.id); + const historicalPools = rewardsByEpoch + .flatMap((rewards) => rewards.rewards.map((reward) => reward.poolId)) + .filter(Boolean) as Cardano.PoolId[]; + const poolsNotInPortfolio = difference(historicalPools, poolsInPortfolio); + + const poolsInPortfolioColoring = poolsInPortfolio.reduce((acc, poolId, index) => { + acc[poolId] = PIE_CHART_DEFAULT_COLOR_SET[index % PIE_CHART_DEFAULT_COLOR_SET.length]!; + return acc; + }, {} as Record); + + const poolsNotInPortfolioColoring = poolsNotInPortfolio.reduce((acc, poolId, index) => { + acc[poolId] = GRAYSCALE_PALETTE[index % GRAYSCALE_PALETTE.length]!; + return acc; + }, {} as Record); + + return { ...poolsInPortfolioColoring, ...poolsNotInPortfolioColoring }; + }, [currentPortfolio, rewardsByEpoch]); + + return (poolId?: Cardano.PoolId) => (poolId ? coloring[poolId] : GRAYSCALE_PALETTE[0]); +}; From e2942711cd507f6213bcdee51845a396e1170cd2 Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 18:40:33 +0100 Subject: [PATCH 11/13] refactor(staking): reorganize rewards chart files into own subfolder --- .../activity/{ => PastEpochsRewards}/PastEpochsRewards.tsx | 4 ++-- .../activity/{ => PastEpochsRewards}/PoolIndicator.tsx | 0 .../activity/{ => PastEpochsRewards}/RewardsChart.tsx | 4 ++-- .../{ => PastEpochsRewards}/RewardsChartTooltip.tsx | 2 +- .../{ => PastEpochsRewards/hooks}/useRewardsByEpoch.ts | 6 +++--- .../hooks}/useRewardsChartPoolsColorMapper.tsx | 0 .../src/features/activity/PastEpochsRewards/index.ts | 1 + 7 files changed, 9 insertions(+), 8 deletions(-) rename packages/staking/src/features/activity/{ => PastEpochsRewards}/PastEpochsRewards.tsx (87%) rename packages/staking/src/features/activity/{ => PastEpochsRewards}/PoolIndicator.tsx (100%) rename packages/staking/src/features/activity/{ => PastEpochsRewards}/RewardsChart.tsx (91%) rename packages/staking/src/features/activity/{ => PastEpochsRewards}/RewardsChartTooltip.tsx (94%) rename packages/staking/src/features/activity/{ => PastEpochsRewards/hooks}/useRewardsByEpoch.ts (94%) rename packages/staking/src/features/activity/{ => PastEpochsRewards/hooks}/useRewardsChartPoolsColorMapper.tsx (100%) create mode 100644 packages/staking/src/features/activity/PastEpochsRewards/index.ts diff --git a/packages/staking/src/features/activity/PastEpochsRewards.tsx b/packages/staking/src/features/activity/PastEpochsRewards/PastEpochsRewards.tsx similarity index 87% rename from packages/staking/src/features/activity/PastEpochsRewards.tsx rename to packages/staking/src/features/activity/PastEpochsRewards/PastEpochsRewards.tsx index ddf087cfca..2fa6547dc0 100644 --- a/packages/staking/src/features/activity/PastEpochsRewards.tsx +++ b/packages/staking/src/features/activity/PastEpochsRewards/PastEpochsRewards.tsx @@ -1,9 +1,9 @@ import { Flex, Text } from '@lace/ui'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { EpochsSwitch } from './EpochsSwitch'; +import { EpochsSwitch } from '../EpochsSwitch'; +import { useRewardsByEpoch } from './hooks/useRewardsByEpoch'; import { RewardsChart } from './RewardsChart'; -import { useRewardsByEpoch } from './useRewardsByEpoch'; const DEFAULT_LAST_EPOCHS = 5; diff --git a/packages/staking/src/features/activity/PoolIndicator.tsx b/packages/staking/src/features/activity/PastEpochsRewards/PoolIndicator.tsx similarity index 100% rename from packages/staking/src/features/activity/PoolIndicator.tsx rename to packages/staking/src/features/activity/PastEpochsRewards/PoolIndicator.tsx diff --git a/packages/staking/src/features/activity/RewardsChart.tsx b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx similarity index 91% rename from packages/staking/src/features/activity/RewardsChart.tsx rename to packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx index 5829a21b10..70c66c9e6a 100644 --- a/packages/staking/src/features/activity/RewardsChart.tsx +++ b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx @@ -1,8 +1,8 @@ import { Card } from '@lace/ui'; import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import type { RewardsByEpoch } from './useRewardsByEpoch'; +import type { RewardsByEpoch } from './hooks/useRewardsByEpoch'; +import { useRewardsChartPoolsColorMapper } from './hooks/useRewardsChartPoolsColorMapper'; import { RewardsChartTooltip } from './RewardsChartTooltip'; -import { useRewardsChartPoolsColorMapper } from './useRewardsChartPoolsColorMapper'; export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { const poolColorMapper = useRewardsChartPoolsColorMapper(chartData); diff --git a/packages/staking/src/features/activity/RewardsChartTooltip.tsx b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChartTooltip.tsx similarity index 94% rename from packages/staking/src/features/activity/RewardsChartTooltip.tsx rename to packages/staking/src/features/activity/PastEpochsRewards/RewardsChartTooltip.tsx index c8f9e95b32..edb87a2c9f 100644 --- a/packages/staking/src/features/activity/RewardsChartTooltip.tsx +++ b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChartTooltip.tsx @@ -2,8 +2,8 @@ import { Card, Flex, Text } from '@lace/ui'; import { useTranslation } from 'react-i18next'; import { TooltipProps } from 'recharts'; import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import { useRewardsChartPoolsColorMapper } from './hooks/useRewardsChartPoolsColorMapper'; import { PoolIndicator } from './PoolIndicator'; -import { useRewardsChartPoolsColorMapper } from './useRewardsChartPoolsColorMapper'; export const RewardsChartTooltip = ({ active, diff --git a/packages/staking/src/features/activity/useRewardsByEpoch.ts b/packages/staking/src/features/activity/PastEpochsRewards/hooks/useRewardsByEpoch.ts similarity index 94% rename from packages/staking/src/features/activity/useRewardsByEpoch.ts rename to packages/staking/src/features/activity/PastEpochsRewards/hooks/useRewardsByEpoch.ts index 7bf904d805..d44ce1c1fe 100644 --- a/packages/staking/src/features/activity/useRewardsByEpoch.ts +++ b/packages/staking/src/features/activity/PastEpochsRewards/hooks/useRewardsByEpoch.ts @@ -6,13 +6,13 @@ import { useOutsideHandles } from 'features/outside-handles-provider'; import { groupBy, sortBy, takeLast, uniqBy } from 'rambda'; import { useEffect, useState } from 'react'; -export type RewardWithMetadata = Omit & { +type RewardWithPoolMetadata = Omit & { metadata: Cardano.StakePoolMetadata | undefined; spendableEpoch: Cardano.EpochNo; - rewards: string; // TODO move the transformation to the chart + rewards: string; }; -export type RewardsByEpoch = { spendableEpoch: Cardano.EpochNo; rewards: RewardWithMetadata[] }[]; +export type RewardsByEpoch = { spendableEpoch: Cardano.EpochNo; rewards: RewardWithPoolMetadata[] }[]; export type UseRewardsByEpochProps = { epochsCount: number; diff --git a/packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx b/packages/staking/src/features/activity/PastEpochsRewards/hooks/useRewardsChartPoolsColorMapper.tsx similarity index 100% rename from packages/staking/src/features/activity/useRewardsChartPoolsColorMapper.tsx rename to packages/staking/src/features/activity/PastEpochsRewards/hooks/useRewardsChartPoolsColorMapper.tsx diff --git a/packages/staking/src/features/activity/PastEpochsRewards/index.ts b/packages/staking/src/features/activity/PastEpochsRewards/index.ts new file mode 100644 index 0000000000..3cdf22ed5a --- /dev/null +++ b/packages/staking/src/features/activity/PastEpochsRewards/index.ts @@ -0,0 +1 @@ +export { PastEpochsRewards } from './PastEpochsRewards'; From 29e17a6d3bfae7f9e093ba444575a8de2e42cb6d Mon Sep 17 00:00:00 2001 From: refi93 Date: Fri, 10 Nov 2023 18:55:15 +0100 Subject: [PATCH 12/13] feat(staking): remove USE_MULTI_DELEGATION_STAKING_ACTIVITY feature flag --- apps/browser-extension-wallet/.env.defaults | 1 - apps/browser-extension-wallet/.env.example | 1 - .../src/features/staking/Navigation.tsx | 18 +++++++----------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/browser-extension-wallet/.env.defaults b/apps/browser-extension-wallet/.env.defaults index 5525b46cb8..fded3830dc 100644 --- a/apps/browser-extension-wallet/.env.defaults +++ b/apps/browser-extension-wallet/.env.defaults @@ -23,7 +23,6 @@ USE_ADA_HANDLE=true USE_DATA_CHECK=false USE_POSTHOG_ANALYTICS=true USE_COMBINED_PASSWORD_NAME_STEP_COMPONENT=false -USE_MULTI_DELEGATION_STAKING_ACTIVITY=false USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT=false USE_MATOMO_ANALYTICS_FOR_OPTED_OUT=false diff --git a/apps/browser-extension-wallet/.env.example b/apps/browser-extension-wallet/.env.example index 76bb6bead5..2cc155c1d3 100644 --- a/apps/browser-extension-wallet/.env.example +++ b/apps/browser-extension-wallet/.env.example @@ -26,7 +26,6 @@ USE_POSTHOG_ANALYTICS=true USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT=false USE_MATOMO_ANALYTICS_FOR_OPTED_OUT=false USE_COMBINED_PASSWORD_NAME_STEP_COMPONENT=false -USE_MULTI_DELEGATION_STAKING_ACTIVITY=false # In App URLs CATALYST_GOOGLE_PLAY_URL=https://play.google.com/store/apps/details?id=io.iohk.vitvoting diff --git a/packages/staking/src/features/staking/Navigation.tsx b/packages/staking/src/features/staking/Navigation.tsx index 7d658b16c1..0f2ae8191f 100644 --- a/packages/staking/src/features/staking/Navigation.tsx +++ b/packages/staking/src/features/staking/Navigation.tsx @@ -73,17 +73,13 @@ export const Navigation = ({ children }: NavigationProps) => { tabIndex={0} highlightWidth="half" /> - {process.env.USE_MULTI_DELEGATION_STAKING_ACTIVITY === 'true' ? ( - - ) : ( - <> - )} + {children(activePage)} From 48668a8a168026d630b614d1cfb94c4d8ec922bf Mon Sep 17 00:00:00 2001 From: januszjanus Date: Fri, 17 Nov 2023 14:46:46 +0100 Subject: [PATCH 13/13] feat(staking): disable chart animation --- .../src/features/activity/PastEpochsRewards/RewardsChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx index 70c66c9e6a..341791fd89 100644 --- a/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx +++ b/packages/staking/src/features/activity/PastEpochsRewards/RewardsChart.tsx @@ -27,7 +27,7 @@ export const RewardsChart = ({ chartData }: { chartData: RewardsByEpoch }) => { `${value} ADA`} /> } /> {Array.from({ length: maxPoolsPerEpochCount }).map((_, i) => ( - + {chartData.map((entry, j) => { const fill = poolColorMapper(entry.rewards[i]?.poolId); return ;