Skip to content

Commit

Permalink
feat(wallet): rebrand staking amount screen (#1881)
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 ui components

* feat: rebrand staking flow: amount screen

* fix type

* feat: update selected style

* feat: improvements
  • Loading branch information
evavirseda authored Aug 21, 2024
1 parent dc40241 commit 0371d94
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 236 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import cx from 'classnames';
import { Info } from '@iota/ui-icons';
import { ValueSize } from './keyValue.enums';
import { Tooltip, TooltipPosition } from '../tooltip';

interface KeyValueProps {
/**
Expand All @@ -20,9 +21,13 @@ interface KeyValueProps {
*/
valueLink?: string;
/**
* Show info icon (optional).
* The tooltip position.
*/
showInfoIcon?: boolean;
tooltipPosition?: TooltipPosition;
/**
* The tooltip text.
*/
tooltipText?: string;
/**
* The supporting label of the KeyValue (optional).
*/
Expand All @@ -36,7 +41,8 @@ interface KeyValueProps {
export function KeyValueInfo({
keyText,
valueText,
showInfoIcon,
tooltipPosition,
tooltipText,
supportingLabel,
valueLink,
size = ValueSize.Small,
Expand All @@ -45,7 +51,11 @@ export function KeyValueInfo({
<div className="flex w-full flex-row items-center justify-between gap-2 py-xxs font-inter">
<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" />}
{tooltipText && (
<Tooltip text={tooltipText} position={tooltipPosition}>
<Info className="text-neutral-60 dark:text-neutral-40" />
</Tooltip>
)}
</div>
<div className="flex w-full flex-row items-baseline justify-end gap-1">
{valueLink ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/ui-kit/src/lib/components/atoms/panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function Panel({
return (
<div
className={cx(
'flex flex-col rounded-xl bg-neutral-100 dark:bg-neutral-10',
'flex w-full flex-col rounded-xl bg-neutral-100 dark:bg-neutral-10',
borderClass,
)}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/ui-kit/src/lib/components/atoms/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Tooltip({
{children}
<div
className={cx(
'absolute z-50 w-max max-w-[200px] rounded bg-neutral-80 p-xs text-neutral-10 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus:opacity-100 dark:bg-neutral-30 dark:text-neutral-92',
'absolute z-[999] hidden w-max max-w-[200px] rounded bg-neutral-80 p-xs text-neutral-10 opacity-0 transition-opacity duration-300 group-hover:flex group-hover:opacity-100 group-focus:opacity-100 dark:bg-neutral-30 dark:text-neutral-92',
tooltipPositionClass,
)}
role="tooltip"
Expand Down
9 changes: 8 additions & 1 deletion apps/ui-kit/src/lib/components/molecules/input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
isVisibilityToggleEnabled ??= inputProps.type === InputType.Password;
const inputRef = useRef<HTMLInputElement | null>(null);

const [hasBlurred, setHasBlurred] = useState<boolean>(false);

const [isInputContentVisible, setIsInputContentVisible] = useState<boolean>(
isContentVisible ?? inputProps.type !== InputType.Password,
);
Expand All @@ -107,6 +109,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}
}

function handleBlur() {
setHasBlurred(true);
}

function assignRefs(element: HTMLInputElement) {
if (ref) {
if (typeof ref === 'function') {
Expand All @@ -123,7 +129,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
label={label}
caption={caption}
disabled={disabled}
errorMessage={errorMessage}
errorMessage={hasBlurred && errorMessage ? errorMessage : ''}
amountCounter={amountCounter}
required={inputProps.required}
>
Expand Down Expand Up @@ -155,6 +161,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
INPUT_PLACEHOLDER_CLASSES,
INPUT_NUMBER_CLASSES,
)}
onBlur={handleBlur}
/>

{supportingText && <SecondaryText>{supportingText}</SecondaryText>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function InputWrapper({
}: React.PropsWithChildren<InputWrapperProps>) {
return (
<div
className={cx('group flex flex-col gap-y-2', {
className={cx('group flex w-full flex-col gap-y-2', {
'cursor-not-allowed opacity-40': disabled,
errored: errorMessage,
enabled: !disabled,
Expand Down
13 changes: 9 additions & 4 deletions apps/ui-kit/src/storybook/stories/atoms/KeyValueInfo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { Meta, StoryObj } from '@storybook/react';
import { KeyValueInfo, ValueSize } from '@/components';
import { KeyValueInfo, TooltipPosition, ValueSize } from '@/components';

const meta: Meta<typeof KeyValueInfo> = {
component: KeyValueInfo,
Expand All @@ -24,7 +24,6 @@ export const Default: Story = {
args: {
keyText: 'Label',
valueText: 'Value',
showInfoIcon: false,
supportingLabel: 'IOTA',
size: ValueSize.Small,
},
Expand All @@ -35,8 +34,14 @@ export const Default: Story = {
valueText: {
control: 'text',
},
showInfoIcon: {
control: 'boolean',
tooltipText: {
control: 'text',
},
tooltipPosition: {
control: {
type: 'select',
options: Object.values(TooltipPosition),
},
},
supportingLabel: {
control: 'text',
Expand Down
159 changes: 68 additions & 91 deletions apps/wallet/src/ui/app/staking/stake/StakeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,24 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { Card } from '_app/shared/card';
import { Text } from '_app/shared/text';
import { NumberInput } from '_components';
import {
NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE,
NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS,
} from '_src/shared/constants';
import { CountDownTimer } from '_src/ui/app/shared/countdown-timer';
import {
createStakeTransaction,
parseAmount,
TimeUnit,
useCoinMetadata,
useFormatCoin,
useGetTimeBeforeEpochNumber,
useTimeAgo,
} from '@iota/core';
import { Field, Form, useFormikContext } from 'formik';
import { memo, useCallback, useMemo } from 'react';

import { useActiveAddress, useTransactionGasBudget } from '../../hooks';
import { type FormValues } from './StakingCard';

const HIDE_MAX = true;
import { Input, InputType, KeyValueInfo, Panel } from '@iota/apps-ui-kit';

export interface StakeFromProps {
validatorAddress: string;
Expand All @@ -33,14 +29,15 @@ export interface StakeFromProps {
}

function StakeForm({ validatorAddress, coinBalance, coinType, epoch }: StakeFromProps) {
const { values, setFieldValue } = useFormikContext<FormValues>();
const { values, setFieldValue, errors } = useFormikContext<FormValues>();

const { data: metadata } = useCoinMetadata(coinType);
const decimals = metadata?.decimals ?? 0;
const [maxToken, symbol, queryResult] = useFormatCoin(coinBalance, coinType);

const transaction = useMemo(() => {
if (!values.amount || !decimals) return null;
if (Number(values.amount) < 0) return null;
const amountWithoutDecimals = parseAmount(values.amount, decimals);
return createStakeTransaction(amountWithoutDecimals, validatorAddress);
}, [values.amount, validatorAddress, decimals]);
Expand All @@ -63,100 +60,80 @@ function StakeForm({ validatorAddress, coinBalance, coinType, epoch }: StakeFrom
const { data: timeBeforeStakeRewardsStarts } =
useGetTimeBeforeEpochNumber(startEarningRewardsEpoch);

const timeBeforeStakeRewardsStartsAgo = useTimeAgo({
timeFrom: timeBeforeStakeRewardsStarts,
shortedTimeLabel: false,
shouldEnd: true,
maxTimeUnit: TimeUnit.ONE_HOUR,
});
const stakedRewardsStartEpoch =
timeBeforeStakeRewardsStarts > 0
? `${timeBeforeStakeRewardsStartsAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsStartsAgo}`
: epoch
? `Epoch #${Number(startEarningRewardsEpoch)}`
: '--';

const { data: timeBeforeStakeRewardsRedeemable } =
useGetTimeBeforeEpochNumber(redeemableRewardsEpoch);
const timeBeforeStakeRewardsRedeemableAgo = useTimeAgo({
timeFrom: timeBeforeStakeRewardsRedeemable,
shortedTimeLabel: false,
shouldEnd: true,
maxTimeUnit: TimeUnit.ONE_HOUR,
});
const timeBeforeStakeRewardsRedeemableAgoDisplay =
timeBeforeStakeRewardsRedeemable > 0
? `${timeBeforeStakeRewardsRedeemableAgo === '--' ? '' : 'in'} ${timeBeforeStakeRewardsRedeemableAgo}`
: epoch
? `Epoch #${Number(redeemableRewardsEpoch)}`
: '--';

return (
<Form className="flex flex-1 flex-col flex-nowrap items-center" autoComplete="off">
<div className="mb-3 mt-3.5 flex w-full flex-col items-center justify-between gap-1.5">
<Text variant="caption" color="gray-85" weight="semibold">
Enter the amount of IOTA to stake
</Text>
<Text variant="bodySmall" color="steel" weight="medium">
Available - {maxToken} {symbol}
</Text>
</div>
<Card
variant="gray"
titleDivider
header={
<div className="flex w-full bg-white p-2.5">
<Field
data-testid="stake-amount-input"
component={NumberInput}
allowNegative={false}
name="amount"
className="text-hero-dark placeholder:text-gray-70 w-full border-none bg-white text-heading4 font-semibold placeholder:font-semibold"
decimals
suffix={` ${symbol}`}
autoFocus
/>
{!HIDE_MAX ? (
<Form
className="flex w-full flex-1 flex-col flex-nowrap items-center gap-md"
autoComplete="off"
>
<Field
name="amount"
render={({ field }: { field: FormValues }) => (
<Input
{...field}
type={InputType.Number}
name="amount"
placeholder="0 IOTA"
caption={coinBalance ? `${maxToken} ${symbol} Available` : ''}
trailingElement={
<button
className="border-gray-60 text-steel-darker hover:border-steel-dark hover:text-steel-darker flex h-6 w-11 cursor-pointer items-center justify-center rounded-2xl border border-solid bg-white text-bodySmall font-medium disabled:cursor-auto disabled:opacity-50"
onClick={setMaxToken}
disabled={queryResult.isPending}
type="button"
disabled={queryResult.isPending}
className="flex items-center justify-center rounded-xl border border-neutral-70 px-sm text-body-md text-neutral-40"
>
Max
</button>
) : null}
</div>
}
footer={
<div className="flex w-full justify-between py-px">
<Text variant="body" weight="medium" color="steel-darker">
Gas Fees
</Text>
<Text variant="body" weight="medium" color="steel-darker">
{gasBudget} {symbol}
</Text>
</div>
}
>
<div className="flex w-full justify-between pb-3.75">
<Text variant="body" weight="medium" color="steel-darker">
Staking Rewards Start
</Text>
{timeBeforeStakeRewardsStarts > 0 ? (
<CountDownTimer
timestamp={timeBeforeStakeRewardsStarts}
variant="body"
color="steel-darker"
weight="semibold"
label="in"
endLabel="--"
/>
) : (
<Text variant="body" weight="medium" color="steel-darker">
{epoch ? `Epoch #${Number(startEarningRewardsEpoch)}` : '--'}
</Text>
)}
</div>
<div className="item-center flex w-full justify-between pb-3.75">
<div className="flex-1">
<Text variant="pBody" weight="medium" color="steel-darker">
Staking Rewards Redeemable
</Text>
</div>
<div className="flex flex-1 items-center justify-end gap-1">
{timeBeforeStakeRewardsRedeemable > 0 ? (
<CountDownTimer
timestamp={timeBeforeStakeRewardsRedeemable}
variant="body"
color="steel-darker"
weight="semibold"
label="in"
endLabel="--"
/>
) : (
<Text variant="body" weight="medium" color="steel-darker">
{epoch ? `Epoch #${Number(redeemableRewardsEpoch)}` : '--'}
</Text>
)}
</div>
}
errorMessage={errors.amount}
label="Amount"
/>
)}
/>
<Panel hasBorder>
<div className="gap-y-sm p-md">
<KeyValueInfo
keyText="Staking Rewards Start"
valueText={stakedRewardsStartEpoch}
/>
<KeyValueInfo
keyText="Redeem Rewards"
valueText={timeBeforeStakeRewardsRedeemableAgoDisplay}
/>
<KeyValueInfo
keyText="Gas fee"
valueText={gasBudget}
supportingLabel={symbol}
/>
</div>
</Card>
</Panel>
</Form>
);
}
Expand Down
Loading

0 comments on commit 0371d94

Please sign in to comment.