Skip to content

Commit

Permalink
feat(wallet): rebrand stake details (#1909)
Browse files Browse the repository at this point in the history
* feat: update view and remove debris

* fix build

* feat: show - instead of ~0

* feat: update selected style

* feat: rebrand stake details

* feat: update buttons

---------

Co-authored-by: JCNoguera <[email protected]>
  • Loading branch information
evavirseda and VmMad authored Aug 21, 2024
1 parent ef5076f commit dc40241
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function KeyValueInfo({
}: KeyValueProps): React.JSX.Element {
return (
<div className="flex w-full flex-row items-center justify-between gap-2 py-xxs font-inter">
<div className="flex flex-row items-center gap-x-0.5">
<div className="flex w-full flex-row items-center gap-x-0.5">
<span className="text-body-md text-neutral-40 dark:text-neutral-60">{keyText}</span>
{showInfoIcon && <Info className="pl-xxxs text-neutral-60" />}
</div>
Expand Down
23 changes: 20 additions & 3 deletions apps/wallet/src/ui/app/components/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useCallback } from 'react';
import type { ReactNode } from 'react';
import { Header } from '@iota/apps-ui-kit';
import { Portal } from '../shared/Portal';
import { useNavigate } from 'react-router-dom';

interface OverlayProps {
title?: string;
Expand All @@ -15,21 +16,37 @@ interface OverlayProps {
closeIcon?: ReactNode | null;
setShowModal?: (showModal: boolean) => void;
background?: 'bg-iota-lightest';
showBackButton?: boolean;
}

export function Overlay({ title, children, showModal, closeOverlay, setShowModal }: OverlayProps) {
export function Overlay({
title,
children,
showModal,
closeOverlay,
showBackButton,
setShowModal,
}: OverlayProps) {
const closeModal = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
closeOverlay && closeOverlay();
setShowModal && setShowModal(false);
},
[closeOverlay, setShowModal],
);

const navigate = useNavigate();
const handleBack = useCallback(() => navigate(-1), [navigate]);
return showModal ? (
<Portal containerId="overlay-portal-container">
<div className="absolute inset-0 z-[9999] flex flex-col flex-nowrap items-center backdrop-blur-[20px]">
{title && <Header titleCentered title={title} onClose={closeModal} />}
{title && (
<Header
onBack={showBackButton ? handleBack : undefined}
titleCentered
title={title}
onClose={closeModal}
/>
)}
<div className="w-full flex-1 overflow-hidden bg-neutral-100 p-md">{children}</div>
</div>
</Portal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,49 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import BottomMenuLayout, { Content } from '_app/shared/bottom-menu-layout';
import { Button } from '_app/shared/ButtonUI';
import { Card } from '_app/shared/card';
import { CardItem } from '_app/shared/card/CardItem';
import { Text } from '_app/shared/text';
import { IconTooltip } from '_app/shared/tooltip';
import { Alert, LoadingIndicator } from '_components';
import { useAppSelector } from '_hooks';
import { ampli } from '_src/shared/analytics/ampli';
import { MIN_NUMBER_IOTA_TO_STAKE } from '_src/shared/constants';
import FaucetRequestButton from '_src/ui/app/shared/faucet/FaucetRequestButton';
import {
useBalance,
useCoinMetadata,
useGetDelegatedStake,
useGetValidatorsApy,
DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
DELEGATED_STAKES_QUERY_STALE_TIME,
useFormatCoin,
formatPercentageDisplay,
} from '@iota/core';
import { useIotaClientQuery } from '@iota/dapp-kit';
import { ArrowLeft16, StakeAdd16, StakeRemove16 } from '@iota/icons';
import { Network, type StakeObject } from '@iota/iota-sdk/client';
import { NANO_PER_IOTA, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { NANO_PER_IOTA, IOTA_TYPE_ARG, formatAddress } from '@iota/iota-sdk/utils';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';

import { useActiveAddress } from '../../hooks/useActiveAddress';
import { Heading } from '../../shared/heading';
import { getDelegationDataByStakeId } from '../getDelegationByStakeId';
import { StakeAmount } from '../home/StakeAmount';
import {
CardImage,
CardBody,
Card,
CardType,
Panel,
KeyValueInfo,
Divider,
Button,
ButtonType,
} from '@iota/apps-ui-kit';
import { ImageIcon } from '../../shared/image-icon';
import { useNavigate } from 'react-router-dom';

interface DelegationDetailCardProps {
validatorAddress: string;
stakedId: string;
}

export function DelegationDetailCard({ validatorAddress, stakedId }: DelegationDetailCardProps) {
const navigate = useNavigate();
const {
data: system,
isPending: loadingValidators,
Expand Down Expand Up @@ -81,7 +87,6 @@ export function DelegationDetailCard({ validatorAddress, stakedId }: DelegationD
}, [allDelegation, stakedId]);

const totalStake = BigInt(delegationData?.principal || 0n);

const iotaEarned = BigInt(
(delegationData as Extract<StakeObject, { estimatedReward: string }>)?.estimatedReward ||
0n,
Expand All @@ -90,6 +95,9 @@ export function DelegationDetailCard({ validatorAddress, stakedId }: DelegationD
apy: 0,
};

const [iotaEarnedFormatted, iotaEarnedSymbol] = useFormatCoin(iotaEarned, IOTA_TYPE_ARG);
const [totalStakeFormatted, totalStakeSymbol] = useFormatCoin(totalStake, IOTA_TYPE_ARG);

const delegationId = delegationData?.status === 'Active' && delegationData?.stakedIotaId;

const stakeByValidatorAddress = `/stake/new?${new URLSearchParams({
Expand Down Expand Up @@ -122,167 +130,90 @@ export function DelegationDetailCard({ validatorAddress, stakedId }: DelegationD
);
}

return (
<div className="flex h-full flex-grow flex-col flex-nowrap">
<BottomMenuLayout>
<Content>
<div className="flex w-full flex-col items-center justify-center">
{hasInactiveValidatorDelegation ? (
<div className="mb-3">
<Alert>
Unstake IOTA from this inactive validator and stake on an active
validator to start earning rewards again.
</Alert>
</div>
) : null}
<div className="flex w-full">
<Card
header={
<div className="divide-gray-45 grid w-full grid-cols-2 divide-x divide-y-0 divide-solid">
<CardItem title="Your Stake">
<StakeAmount balance={totalStake} variant="heading5" />
</CardItem>

<CardItem title="Earned">
<StakeAmount
balance={iotaEarned}
variant="heading5"
isEarnedRewards
/>
</CardItem>
</div>
}
padding="none"
>
<div className="divide-gray-45 flex divide-x divide-y-0 divide-solid">
<CardItem
title={
<div className="text-steel-darker flex items-start gap-1">
APY
<div className="text-steel">
<IconTooltip
tip="Annual Percentage Yield"
placement="top"
/>
</div>
</div>
}
>
<div className="flex items-baseline gap-0.5">
<Heading
variant="heading4"
weight="semibold"
color="gray-90"
leading="none"
>
{isApyApproxZero ? '~' : ''}
{apy}
</Heading>

<Text
variant="subtitleSmall"
weight="medium"
color="steel-dark"
>
%
</Text>
</div>
</CardItem>

<CardItem
title={
<div className="text-steel-darker flex gap-1">
Commission
<div className="text-steel">
<IconTooltip
tip="Validator commission"
placement="top"
/>
</div>
</div>
}
>
<div className="flex items-baseline gap-0.5">
<Heading
variant="heading4"
weight="semibold"
color="gray-90"
leading="none"
>
{commission}
</Heading>
if (hasInactiveValidatorDelegation) {
<div className="mb-3">
<Alert>
Unstake IOTA from this inactive validator and stake on an active validator to start
earning rewards again.
</Alert>
</div>;
}

<Text
variant="subtitleSmall"
weight="medium"
color="steel-dark"
>
%
</Text>
</div>
</CardItem>
</div>
</Card>
</div>
<div className="my-3.75 flex w-full gap-2.5">
{!hasInactiveValidatorDelegation ? (
<Button
size="tall"
variant="outline"
to={stakeByValidatorAddress}
before={<StakeAdd16 />}
text="Stake IOTA"
onClick={() => {
ampli.clickedStakeIota({
isCurrentlyStaking: true,
sourceFlow: 'Delegation detail card',
});
}}
disabled={showRequestMoreIotaToken}
/>
) : null}
function handleAddNewStake() {
navigate(stakeByValidatorAddress);
ampli.clickedStakeIota({
isCurrentlyStaking: true,
sourceFlow: 'Delegation detail card',
});
}

{Boolean(totalStake) && delegationId && (
<Button
data-testid="unstake-button"
size="tall"
variant="outline"
to={stakeByValidatorAddress + '&unstake=true'}
onClick={() => {
ampli.clickedUnstakeIota({
stakedAmount: Number(totalStake / NANO_PER_IOTA),
validatorAddress,
});
}}
text="Unstake IOTA"
before={<StakeRemove16 />}
/>
)}
</div>
</div>
</Content>
function handleUnstake() {
navigate(stakeByValidatorAddress + '&unstake=true');
ampli.clickedUnstakeIota({
stakedAmount: Number(totalStake / NANO_PER_IOTA),
validatorAddress,
});
}

{/* show faucet request button on devnet or testnet whenever there is only one coin */}
{showRequestMoreIotaToken ? (
<div className="flex flex-col items-center gap-4">
<div className="w-8/12 text-center">
<Text variant="pSubtitle" weight="medium" color="steel-darker">
You need a minimum of {MIN_NUMBER_IOTA_TO_STAKE} IOTA to continue
staking.
</Text>
</div>
<FaucetRequestButton size="tall" />
return (
<div className="flex h-full flex-col justify-between">
<div className="flex flex-col gap-y-md">
<Card type={CardType.Filled}>
<CardImage>
<ImageIcon
src={null}
label={validatorData?.name || ''}
fallback={validatorData?.name || ''}
/>
</CardImage>
<CardBody
title={validatorData?.name || ''}
subtitle={formatAddress(validatorAddress)}
/>
</Card>
<Panel hasBorder>
<div className="flex flex-col gap-y-sm p-md">
<KeyValueInfo
keyText="Your Stake"
valueText={totalStakeFormatted}
supportingLabel={totalStakeSymbol}
/>
<KeyValueInfo
keyText="Earned"
valueText={iotaEarnedFormatted}
supportingLabel={iotaEarnedSymbol}
/>
<Divider />
<KeyValueInfo
keyText="APY"
valueText={formatPercentageDisplay(apy, '--', isApyApproxZero)}
/>
<KeyValueInfo
keyText="Commission"
valueText={`${commission.toString()}%`}
/>
</div>
) : (
</Panel>
</div>
<div className="my-3.75 flex w-full gap-2.5">
{Boolean(totalStake) && delegationId && (
<Button
size="tall"
variant="secondary"
to="/stake"
before={<ArrowLeft16 />}
text="Back"
type={ButtonType.Secondary}
onClick={handleUnstake}
text="Unstake"
fullWidth
/>
)}
</BottomMenuLayout>
{!hasInactiveValidatorDelegation ? (
<Button
type={ButtonType.Primary}
text="Stake"
onClick={handleAddNewStake}
disabled={showRequestMoreIotaToken}
fullWidth
/>
) : null}
</div>
</div>
);
}
Loading

0 comments on commit dc40241

Please sign in to comment.