Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions apps/browser-extension-wallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OutsideHandlesProvider, Staking } from '@lace/staking';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import {
useAnalyticsContext,
useBackgroundServiceAPIContext,
Expand All @@ -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();
Expand All @@ -38,7 +41,9 @@ export const MultiDelegationStaking = (): JSX.Element => {
fetchNetworkInfo,
networkInfo,
blockchainProvider,
currentChain
currentChain,
activityDetail,
resetActivityState
} = useWalletStore((state) => ({
getKeyAgentType: state.getKeyAgentType,
inMemoryWallet: state.inMemoryWallet,
Expand All @@ -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 = {
Expand All @@ -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(
Expand All @@ -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 (
<OutsideHandlesProvider
{...{
Expand All @@ -106,6 +119,7 @@ export const MultiDelegationStaking = (): JSX.Element => {
walletStoreNetworkInfo: networkInfo,
walletStoreBlockchainProvider: blockchainProvider,
walletStoreWalletActivities: walletActivities,
walletStoreWalletActivitiesStatus: walletActivitiesStatus,
// TODO: LW-7575 make compactNumber reusable and not pass it here.
compactNumber: compactNumberWithUnit,
multidelegationFirstVisit,
Expand All @@ -118,6 +132,27 @@ export const MultiDelegationStaking = (): JSX.Element => {
}}
>
<Staking currentChain={currentChain} theme={theme.name} />
{/*
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?).
*/}
<Drawer
visible={!!activityDetail}
onClose={resetActivityState}
navigation={
<DrawerNavigation
title={t('transactions.detail.title')}
onCloseIconClick={() => {
resetActivityState();
}}
/>
}
>
{activityDetail && priceResult && <ActivityDetail price={priceResult} />}
</Drawer>
</OutsideHandlesProvider>
);
};
1 change: 1 addition & 0 deletions packages/common/src/analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions packages/staking/src/features/activity/Activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 = () => {
const { walletStoreWalletActivitiesStatus: walletActivitiesStatus, walletStoreWalletActivities: walletActivities } =
useOutsideHandles();
const groupedRewardsActivities = getGroupedRewardsActivities(walletActivities);

return (
<>
<PastEpochsRewards />
{walletActivitiesStatus === StateStatus.LOADED && groupedRewardsActivities.length === 0 ? (
<NoStakingActivity />
) : (
<RewardsHistory
walletActivitiesStatus={walletActivitiesStatus}
groupedRewardsActivities={groupedRewardsActivities}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.buttonsBackground {
background-color: var(--light-mode-light-grey, var(--dark-mode-dark-grey, #2f2f2f));
border-radius: 1rem;
}
23 changes: 23 additions & 0 deletions packages/staking/src/features/activity/EpochsSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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];

export type EpochsSwitchProps = {
epochsCount: number;
setEpochsCount: (epochsCount: number) => void;
};

export const EpochsSwitch = ({ epochsCount, setEpochsCount }: EpochsSwitchProps) => (
<Flex gap="$8" alignItems="center">
<Text.Body.Normal>Epochs:</Text.Body.Normal>
<Flex p="$8" gap="$8" alignItems="center" className={styles.buttonsBackground}>
{EPOCHS_OPTIONS.map((option, i) => {
const activeOption = epochsCount === option;
const Component = activeOption ? ControlButton.Filled : ControlButton.Outlined;
return <Component key={i} label={`Last ${option}`} onClick={() => setEpochsCount(option)} />;
})}
</Flex>
</Flex>
);
13 changes: 13 additions & 0 deletions packages/staking/src/features/activity/NoStakingActivity.css.ts
Original file line number Diff line number Diff line change
@@ -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,
});
17 changes: 17 additions & 0 deletions packages/staking/src/features/activity/NoStakingActivity.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex h="$fill" flexDirection="column" alignItems="center" justifyContent="center" gap="$8">
<SadFaceIcon className={styles.sadFaceIcon} />
<Typography.Text className={styles.noActivityText}>
{t('activity.rewardsHistory.noStakingActivityYet')}
</Typography.Text>
</Flex>
);
};
23 changes: 23 additions & 0 deletions packages/staking/src/features/activity/PastEpochsRewards.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Flex mb="$32" justifyContent="space-between" alignItems="center">
<Text.SubHeading>{t('activity.rewardsChart.title')}</Text.SubHeading>
<EpochsSwitch epochsCount={epochsCount} setEpochsCount={setEpochsCount} />
</Flex>
{rewardsByEpoch && <RewardsChart chartData={rewardsByEpoch} />}
</>
);
};
7 changes: 7 additions & 0 deletions packages/staking/src/features/activity/PoolIndicator.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<svg width="4" height="41" viewBox="0 0 4 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="0.5" width="40" height="4" rx="2" transform="rotate(90 4 0.5)" fill={color} />
</svg>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.chartContainer {
height: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.chartContainer {
height: auto;
}
42 changes: 42 additions & 0 deletions packages/staking/src/features/activity/RewardsChart.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card.Outlined>
<ResponsiveContainer width="100%" aspect={2.4} height="auto">
<BarChart
width={500}
height={300}
data={chartData}
margin={{
bottom: 32,
left: 24,
right: 24,
top: 32,
}}
>
<XAxis dataKey="epoch" tickLine={false} axisLine={false} tickMargin={16} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(value) => `${value} ADA`} />
<Tooltip cursor={false} content={<RewardsChartTooltip />} />
{maxPoolsIterator.map((_, i) => (
<Bar key={i} dataKey={`rewards[${i}].rewards`} stackId="a" maxBarSize={24}>
{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 <Cell key={`cell-${j}`} fill={fill} />;
})}
</Bar>
))}
</BarChart>
</ResponsiveContainer>
</Card.Outlined>
);
};
38 changes: 38 additions & 0 deletions packages/staking/src/features/activity/RewardsChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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<ValueType, NameType>) => {
const { checkIfPoolIsInPortfolio } = usePoolInPortfolioPresence();
if (active && payload && payload.length > 0) {
return (
<Card.Elevated className="custom-tooltip">
<Flex flexDirection="column" px="$16" py="$8">
<Text.Body.Small weight="$semibold">Epoch {label}</Text.Body.Small>
<Flex flexDirection="column" gap="$4">
{maxPoolsIterator.map((_, i) => {
const poolId = payload[i]?.payload?.rewards?.[i]?.poolId;
const poolInPortfolio = payload[i] && checkIfPoolIsInPortfolio(poolId);
return (
payload[i] && (
<Flex gap="$8" key={i} alignItems="center">
<PoolIndicator color={poolInPortfolio ? PIE_CHART_DEFAULT_COLOR_SET[i] : GRAYSCALE_PALETTE[i]} />
<Flex flexDirection="column">
<Text.Body.Small>{payload[i]?.payload?.rewards?.[i]?.metadata.name}</Text.Body.Small>
<Text.Body.Small>Rewards: {payload[i]?.value} ADA</Text.Body.Small>
</Flex>
</Flex>
)
);
})}
</Flex>
</Flex>
</Card.Elevated>
);
}

return null;
};
Loading