diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/AccountCooldowns/AccountCooldowns.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/AccountCooldowns/AccountCooldowns.tsx index dee829be..bbfd2e7f 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/AccountCooldowns/AccountCooldowns.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/AccountCooldowns/AccountCooldowns.tsx @@ -22,7 +22,7 @@ export default function AccountCooldowns({ cooldowns }: Props) { {cooldowns.map((c) => { const cdDays = timestampToRelativeDays(c.timestamp); return ( - + {t('inactiveStake.label')} {t('inactiveStake.description')} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss index 94d8debd..175a5fcb 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss @@ -7,59 +7,7 @@ .register-delegator { &__token-card { margin-top: rem(16px); - background: $gradient-card-bg; - border-radius: rem(16px); - padding: rem(20px) rem(16px); margin-bottom: rem(8px); - - .token { - margin-bottom: rem(12px); - - .token-available { - display: flex; - margin-top: rem(12px); - justify-content: space-between; - } - } - - .amount { - padding: rem(24px) 0 rem(8px) 0; - border-top: 1px solid rgba($color-black, 0.1); - border-bottom: 1px solid rgba($color-black, 0.1); - - .amount-selected { - display: flex; - margin-top: rem(8px); - justify-content: space-between; - align-items: center; - - .capture__additional_small { - border-radius: rem(4px); - padding: rem(4px); - background-color: $secondary-button-bg; - } - } - } - - .estimated-fee { - display: flex; - flex-direction: column; - margin-top: rem(4px); - } - - .text__main_regular, - .text__main_small, - .capture__additional_small { - color: $color-black; - } - - .heading_big { - color: $color-grey-1; - } - - .capture__main_small { - color: $color-grey-3; - } } &__pool-info { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index ebbd4b8a..3c9649b5 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -1,9 +1,15 @@ -import React, { useMemo } from 'react'; +/* eslint-disable react/destructuring-assignment */ +import React, { useEffect, useMemo, useState } from 'react'; import { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useAtomValue } from 'jotai'; import { useAsyncMemo } from 'wallet-common-helpers'; -import { AccountTransactionType, DelegationTargetType } from '@concordium/web-sdk'; +import { + AccountTransactionType, + CcdAmount, + ConfigureDelegationPayload, + DelegationTargetType, +} from '@concordium/web-sdk'; import Button from '@popup/popupX/shared/Button'; import FormToggleCheckbox from '@popup/popupX/shared/Form/ToggleCheckbox'; @@ -12,13 +18,15 @@ import Form, { useForm } from '@popup/popupX/shared/Form'; import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; -import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; +import { formatCcdAmount, formatTokenAmount, parseCcdAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; import { grpcClientAtom } from '@popup/store/settings'; import Text from '@popup/popupX/shared/Text'; import { useGetTransactionFee } from '@popup/shared/utils/transaction-helpers'; +import FullscreenNotice, { FullscreenNoticeProps } from '@popup/popupX/shared/FullscreenNotice'; -import { DelegationTypeForm, DelegatorStakeForm, configureDelegatorPayloadFromForm } from '../util'; +import { DelegationTypeForm, DelegatorForm, DelegatorStakeForm, configureDelegatorPayloadFromForm } from '../util'; +import { STAKE_WARNING_THRESHOLD, isAboveStakeWarningThreshold } from '../../util'; type PoolInfoProps = { /** The validator pool ID to show information for */ @@ -60,17 +68,40 @@ function PoolInfo({ validatorId }: PoolInfoProps) { ); } +type HighStakeNoticeProps = FullscreenNoticeProps & { + onContinue(): void; +}; + +function HighStakeWarning({ onContinue, ...props }: HighStakeNoticeProps) { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.stake.overStakeThresholdWarning' }); + return ( + + + + {t('description', { threshold: STAKE_WARNING_THRESHOLD.toString() })} + + + + + + + ); +} + type Props = { /** The title for the configuriation step */ title: string; /** The initial values of the step, if any */ initialValues?: DelegatorStakeForm; + /** The delegation target of the transaction */ target: DelegationTypeForm; + /** The existing delegation values registered on the account */ + existingValues: DelegatorForm | undefined; /** The submit handler triggered when submitting the form in the step */ onSubmit(values: DelegatorStakeForm): void; }; -export default function DelegatorStake({ title, target, initialValues, onSubmit }: Props) { +export default function DelegatorStake({ title, target, initialValues, existingValues, onSubmit }: Props) { const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.stake' }); const form = useForm({ defaultValues: initialValues ?? { amount: '0.00', redelegate: true }, @@ -78,49 +109,104 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit const submit = form.handleSubmit(onSubmit); const selectedCred = useSelectedCredential(); const selectedAccountInfo = useAccountInfo(selectedCred); + const [highStakeWarning, setHighStakeWarning] = useState(false); + + const values = form.watch(); const getCost = useGetTransactionFee(AccountTransactionType.ConfigureDelegation); - const fee = useMemo( - () => getCost(configureDelegatorPayloadFromForm({ target, stake: { amount: '0', redelegate: true } })), // Use dummy values, as it does not matter when calculating transaction cost - [target, getCost] - ); + const fee = useMemo(() => { + let payload: ConfigureDelegationPayload; + try { + // We try here, as parsing invalid CCD amounts from the input can fail. + payload = configureDelegatorPayloadFromForm({ target, stake: values }, existingValues); + } catch { + // Fall back to a payload from a form with any parsable amount + payload = configureDelegatorPayloadFromForm( + { + target: { + type: target?.type ?? DelegationTargetType.PassiveDelegation, + bakerId: target?.bakerId, + }, + stake: { amount: '0', redelegate: values.redelegate ?? false }, + }, + existingValues + ); + } + return getCost(payload); + }, [target, values, getCost]); + + useEffect(() => { + if (selectedAccountInfo === undefined || fee === undefined) { + return; + } + + try { + const parsed = parseCcdAmount(values.amount); + const newMax = CcdAmount.fromMicroCcd( + selectedAccountInfo.accountAmount.microCcdAmount - fee.microCcdAmount + ); + if (parsed.microCcdAmount > newMax.microCcdAmount) { + form.setValue('amount', formatCcdAmount(newMax), { shouldValidate: true }); + } + } catch { + // Do nothing.. + } + }, [selectedAccountInfo?.accountAmount, fee]); if (selectedAccountInfo === undefined || selectedCred === undefined || fee === undefined) { return null; } + const handleSubmit = () => { + if (!form.formState.errors.amount === undefined) { + submit(); // To set the form to submitted. + return; + } + + const amount = parseCcdAmount(form.getValues().amount); + if (isAboveStakeWarningThreshold(amount.microCcdAmount, selectedAccountInfo)) { + setHighStakeWarning(true); + } else { + submit(); + } + }; + return ( - - - - {t('selectedAccount', { account: displayNameAndSplitAddress(selectedCred) })} - -
- {(f) => ( - <> - } - ccdBalance="total" - /> - {target.type === DelegationTargetType.Baker && ( - - )} -
-
- {t('redelegate.label')} - + <> + setHighStakeWarning(false)} onContinue={submit} /> + + + + {t('selectedAccount', { account: displayNameAndSplitAddress(selectedCred) })} + + + {(f) => ( + <> + } + ccdBalance="total" + /> + {target.type === DelegationTargetType.Baker && ( + + )} +
+
+ {t('redelegate.label')} + +
+ {t('redelegate.description')}
- {t('redelegate.description')} -
- - )} - - - - - + + )} + + + + + + ); } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow.tsx new file mode 100644 index 00000000..9b0c5d15 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow.tsx @@ -0,0 +1,128 @@ +/* eslint-disable react/destructuring-assignment */ +import React, { useCallback, useState } from 'react'; +import { Location, Navigate, useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { AccountInfoType, DelegationTargetType } from '@concordium/web-sdk'; + +import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import MultiStepForm from '@popup/shared/MultiStepForm'; +import { useSelectedAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; +import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; +import FullscreenNotice, { FullscreenNoticeProps } from '@popup/popupX/shared/FullscreenNotice'; +import Page from '@popup/popupX/shared/Page'; +import Button from '@popup/popupX/shared/Button'; +import DelegatorStake from './Stake'; +import DelegatorType from './Type'; +import { configureDelegatorPayloadFromForm, type DelegatorForm } from './util'; +import { DelegationResultLocationState } from './Result/DelegationResult'; + +function NoChangesNotice(props: FullscreenNoticeProps) { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.update.noChangesNotice' }); + return ( + + + + {t('description')} + + + + + + ); +} + +type Props = { + title: string; + existingValues?: DelegatorForm | undefined; +}; + +function DelegatorTransactionFlow({ existingValues, title }: Props) { + const { state, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; + const nav = useNavigate(); + const [noChangesNotice, setNoChangesNotice] = useState(false); + + const initialValues = state ?? existingValues; + const store = useState>(initialValues ?? {}); + + const handleDone = useCallback( + (form: DelegatorForm) => { + const payload = configureDelegatorPayloadFromForm(form, existingValues); + + if (Object.values(payload).every((v) => v === undefined)) { + setNoChangesNotice(true); + return; + } + + nav(pathname, { replace: true, state: form }); // Override current router entry with stateful version + + const submitDelegatorState: DelegationResultLocationState = { + payload, + type: existingValues !== undefined ? 'change' : 'register', + }; + nav(absoluteRoutes.settings.earn.delegator.submit.path, { state: submitDelegatorState }); + }, + [pathname, existingValues, setNoChangesNotice] + ); + + return ( + <> + setNoChangesNotice(false)} /> + onDone={handleDone} valueStore={store}> + {{ + target: { + render: (initial, onNext) => ( + + ), + }, + stake: { + render: (initial, onNext, form) => { + if (form.target === undefined) { + return ; + } + + return ( + + ); + }, + }, + }} + + + ); +} + +export function RegisterDelegatorTransactionFlow() { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.register' }); + return ; +} + +export function UpdateDelegatorTransactionFlow() { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.update' }); + const accountInfo = useSelectedAccountInfo(); + + if (accountInfo === undefined || accountInfo.type !== AccountInfoType.Delegator) { + return null; + } + const { + accountDelegation: { stakedAmount, restakeEarnings, delegationTarget }, + } = accountInfo; + + const values: DelegatorForm = { + stake: { + amount: formatCcdAmount(stakedAmount), + redelegate: restakeEarnings, + }, + target: + delegationTarget.delegateType === DelegationTargetType.PassiveDelegation + ? { type: DelegationTargetType.PassiveDelegation } + : { type: DelegationTargetType.Baker, bakerId: delegationTarget.bakerId.toString() }, + }; + + return ; +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx deleted file mode 100644 index a055d8d9..00000000 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useCallback } from 'react'; -import { Location, Navigate, useLocation, useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -import { absoluteRoutes } from '@popup/popupX/constants/routes'; -import MultiStepForm from '@popup/shared/MultiStepForm'; -import DelegatorStake from '../Stake'; -import DelegatorType from '../Type'; -import { configureDelegatorPayloadFromForm, type DelegatorForm } from '../util'; -import { DelegationResultLocationState } from '../Result/DelegationResult'; - -export default function DelegatorTransactionFlow() { - const { state: initialValues, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; - const nav = useNavigate(); - const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.register' }); - - const handleDone = useCallback( - (values: DelegatorForm) => { - const payload = configureDelegatorPayloadFromForm(values); - - nav(pathname, { replace: true, state: values }); // Override current router entry with stateful version - - const submitDelegatorState: DelegationResultLocationState = { payload, type: 'register' }; - nav(absoluteRoutes.settings.earn.delegator.submit.path, { state: submitDelegatorState }); - }, - [pathname] - ); - - return ( - onDone={handleDone} initialValues={initialValues}> - {{ - target: { - render: (initial, onNext) => ( - - ), - }, - stake: { - render: (initial, onNext, form) => { - if (form.target === undefined) { - return ; - } - - return ( - - ); - }, - }, - }} - - ); -} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts deleted file mode 100644 index 63d943bb..00000000 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './TransactionFlow'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index 03a3f8ce..5dddb9a6 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -2,13 +2,15 @@ import React from 'react'; import { FormRadio } from '@popup/popupX/shared/Form/Radios'; import { Trans, useTranslation } from 'react-i18next'; import { Validate } from 'react-hook-form'; +import { useAtomValue } from 'jotai'; +import { DelegationTargetType, OpenStatusText } from '@concordium/web-sdk'; + import Page from '@popup/popupX/shared/Page'; import ExternalLink from '@popup/popupX/shared/ExternalLink'; -import { DelegationTargetType, OpenStatusText } from '@concordium/web-sdk'; import Form, { useForm } from '@popup/popupX/shared/Form'; import Button from '@popup/popupX/shared/Button'; import FormInput from '@popup/popupX/shared/Form/Input/Input'; -import { useAtomValue } from 'jotai'; +import Text from '@popup/popupX/shared/Text'; import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; import { DelegationTypeForm } from '../util'; @@ -48,7 +50,7 @@ export default function DelegationType({ initialValues, onSubmit, title }: Props return ( - {t('description')} + {t('description')} className="delegation-type__select-form" formMethods={form} onSubmit={onSubmit}> {(f) => ( <> @@ -81,7 +83,7 @@ export default function DelegationType({ initialValues, onSubmit, title }: Props )} - + {target === DelegationTargetType.PassiveDelegation ? ( )} - + diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts index ae6f8404..23a9080a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts @@ -1,4 +1,4 @@ -import { ConfigureDelegationPayload, DelegationTarget, DelegationTargetType } from '@concordium/web-sdk'; +import { CcdAmount, ConfigureDelegationPayload, DelegationTarget, DelegationTargetType } from '@concordium/web-sdk'; import { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { parseCcdAmount } from '@popup/popupX/shared/utils/helpers'; @@ -24,20 +24,40 @@ export type DelegatorForm = { stake: DelegatorStakeForm; }; -/** Constructs a {@linkcode ConfigureDelegationPayload} from the corresponding {@linkcode DelegatorForm} */ -export function configureDelegatorPayloadFromForm(values: DelegatorForm): ConfigureDelegationPayload { - let delegationTarget: DelegationTarget; - if (values.target.type === DelegationTargetType.PassiveDelegation) { - delegationTarget = { delegateType: DelegationTargetType.PassiveDelegation }; - } else if (values.target.bakerId === undefined) { - throw new Error('Expected bakerId to be defined'); - } else { - delegationTarget = { delegateType: DelegationTargetType.Baker, bakerId: BigInt(values.target.bakerId) }; +/** + * Constructs a {@linkcode ConfigureDelegationPayload} from the corresponding {@linkcode DelegatorForm} + * @throws if the given `values.state.amount` cannot be parsed. + */ +export function configureDelegatorPayloadFromForm( + values: DelegatorForm, + existingValues?: DelegatorForm +): ConfigureDelegationPayload { + let delegationTarget: DelegationTarget | undefined; + if ( + existingValues === undefined || + values.target.type !== existingValues.target.type || + values.target.bakerId !== existingValues.target.bakerId + ) { + if (values.target.type === DelegationTargetType.PassiveDelegation) { + delegationTarget = { delegateType: DelegationTargetType.PassiveDelegation }; + } else if (values.target.bakerId === undefined) { + throw new Error('Expected bakerId to be defined'); + } else { + delegationTarget = { delegateType: DelegationTargetType.Baker, bakerId: BigInt(values.target.bakerId) }; + } + } + let restakeEarnings: boolean | undefined; + if (existingValues === undefined || values.stake.redelegate !== existingValues.stake.redelegate) { + restakeEarnings = values.stake.redelegate; + } + let stake: CcdAmount.Type | undefined; + if (existingValues === undefined || values.stake.amount !== existingValues.stake.amount) { + stake = parseCcdAmount(values.stake.amount); } return { - restakeEarnings: values.stake.redelegate, - stake: parseCcdAmount(values.stake.amount), + restakeEarnings, + stake, delegationTarget, }; } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx index 22db4d07..f1fa085a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx @@ -17,15 +17,17 @@ export default function EarningRewards() { const cp = useBlockChainParameters(); const accountInfo = useSelectedAccountInfo(); - if (cp === undefined) { - return null; + switch (accountInfo?.type) { + case AccountInfoType.Delegator: + return ; + case AccountInfoType.Baker: + return ; + default: + break; } - if (accountInfo?.type === AccountInfoType.Delegator) { - return ; - } - if (accountInfo?.type === AccountInfoType.Baker) { - return ; + if (cp === undefined) { + return null; } const bakingThreshold = cpBakingThreshold(cp); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts index 689137f1..8f7e4e3e 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts @@ -69,7 +69,14 @@ const t = { backTitle: 'Earning rewards', notice: 'This will lock your delegation amount. Amount is released after {{cooldown}} days from the time you remove or decrease your delegation.', }, - update: { title: 'Update delegation' }, + update: { + title: 'Update delegation', + noChangesNotice: { + title: 'No changes', + description: 'The proposed transaction contains no changes compared to the current delegation.', + buttonBack: 'Go back', + }, + }, target: { description: 'You can delegate to an open pool of your choice, or you can stake using passive delegation.', radioValidatorLabel: 'Validator', @@ -115,6 +122,13 @@ const t = { description: 'I want to automatically add my delegation rewards to my delegation amount.', }, buttonContinue: 'Continue', + overStakeThresholdWarning: { + title: 'Important', + description: + 'You are about to lock more than {{ threshold }}% of your total balance in a delegation stake.\n\nIf you don’t have enough unlocked CCD at your disposal, you might not be able to pay future transaction fees.', + buttonContinue: 'Continue', + buttonBack: 'Enter new stake', + }, }, submit: { backTitle: 'Delegation settings', diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/util.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/util.ts new file mode 100644 index 00000000..b182ca18 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/util.ts @@ -0,0 +1,8 @@ +import { AccountInfo } from '@concordium/web-sdk'; + +/* The percentage threshold, over which the user should be displayed an error, if they attempt to stake that much */ +export const STAKE_WARNING_THRESHOLD = 95n; + +export function isAboveStakeWarningThreshold(amount: bigint, accountInfo: AccountInfo): boolean { + return amount * 100n > accountInfo.accountAmount.microCcdAmount * STAKE_WARNING_THRESHOLD; +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.scss b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.scss index 0cfae607..2c8ab075 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.scss @@ -3,7 +3,6 @@ flex-direction: column; border-radius: rem(16px); background: $gradient-card-bg; - margin-top: rem(16px); padding: rem(20px) rem(16px); .text__main_medium, diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx index 16a06991..6d13944a 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx @@ -11,7 +11,7 @@ import SideArrow from '@assets/svgX/side-arrow.svg'; import ConcordiumLogo from '@assets/svgX/concordium-logo.svg'; import { validateAccountAddress, validateTransferAmount } from '@popup/shared/utils/transaction-helpers'; import Img, { DEFAULT_FAILED } from '@popup/shared/Img'; -import { displayAsCcd } from 'wallet-common-helpers'; +import { ClassName, displayAsCcd } from 'wallet-common-helpers'; import Text from '@popup/popupX/shared/Text'; import { RequiredUncontrolledFieldProps } from '../common/types'; import { makeUncontrolled } from '../common/utils'; @@ -237,7 +237,8 @@ export type TokenAmountViewProps = { */ onSelectToken(event: TokenSelectEvent): void; } & ValueVariant & - TokenVariant; + TokenVariant & + ClassName; /** * TokenAmount component renders a form for transferring tokens with an amount field and optionally a receiver field. @@ -246,7 +247,7 @@ export type TokenAmountViewProps = { */ export default function TokenAmountView(props: TokenAmountViewProps) { const { t } = useTranslation('x', { keyPrefix: 'sharedX' }); - const { buttonMaxLabel, fee, tokens, balance, onSelectToken } = props; + const { buttonMaxLabel, fee, tokens, balance, onSelectToken, className } = props; const [selectedToken, setSelectedToken] = useState(() => { switch (props.tokenType) { case 'cis2': { @@ -352,7 +353,7 @@ export default function TokenAmountView(props: TokenAmountViewProps) { ); return ( -
+
{t('form.tokenAmount.token.label')} {props.tokenType !== undefined ? ( diff --git a/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.scss b/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.scss index f897151b..f44c4b6c 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.scss @@ -9,6 +9,7 @@ &__back { border: none; background: none; + padding: rem(5px) 0 rem(3px); &:hover { background: none !important; diff --git a/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.tsx b/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.tsx index e5e9fca5..c57778fd 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/FullscreenNotice/FullscreenNotice.tsx @@ -27,7 +27,7 @@ function Header({ isScrolling, onBack }: HeaderProps) { const htmlElement = document.getElementsByTagName('html')[0]!; const bodyElement = document.getElementsByTagName('body')[0]!; -type Props = { +export type FullscreenNoticeProps = { /** Control whether notice is shown or not */ open: boolean; /** Invoked when the notice is closed */ @@ -50,7 +50,11 @@ type Props = { * This content is shown in a modal! * */ -export default function FullscreenNotice({ open, onClose, children }: PropsWithChildren): JSX.Element | null { +export default function FullscreenNotice({ + open, + onClose, + children, +}: PropsWithChildren): JSX.Element | null { const [scroll, setScroll] = React.useState(0); const isScrolling = useMemo(() => scroll > 0, [!!scroll]); const close = useCallback(() => { @@ -78,7 +82,7 @@ export default function FullscreenNotice({ open, onClose, children }: PropsWithC } return ( - +
} /> } + element={} /> } + element={} path={`${relativeRoutes.settings.earn.delegator.update.path}/*`} /> TODO} path={`${relativeRoutes.settings.earn.delegator.stop.path}`} /> diff --git a/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx b/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx index d3668e7f..47e80396 100644 --- a/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx +++ b/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { Routes, useNavigate, Route, useLocation } from 'react-router-dom'; import { isDefined, noOp, useUpdateEffect } from 'wallet-common-helpers'; @@ -35,7 +35,7 @@ export interface FormChild { * Function to render page component responsible for letting user fill out the respective substate. * This is a function to avoid anonymous components messing up render tree updates. */ - render(initial: F[K] | undefined, onNext: (values: F[K]) => void, formValues: Partial): JSX.Element; + render(initial: F[K] | undefined, onNext: (values: F[K]) => void, formValues: Partial): ReactNode; } export type FormChildren> = { diff --git a/packages/browser-wallet/src/popup/styles/elements/_base.scss b/packages/browser-wallet/src/popup/styles/elements/_base.scss index ff4d160b..681d0c72 100644 --- a/packages/browser-wallet/src/popup/styles/elements/_base.scss +++ b/packages/browser-wallet/src/popup/styles/elements/_base.scss @@ -89,6 +89,9 @@ body { #root { height: 100%; +} + +:is(:not(.popup-x)) #root { @include transition(filter); .modal-open & {