Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Send): Fee breakdown table #606

Merged
merged 31 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0272924
Add `AccordionInfo` component
httpiga Jan 30, 2023
7916523
Add fee breakdown UI
httpiga Feb 4, 2023
33809b0
Wrap `loadFeeConfigValues` in hook
httpiga Feb 18, 2023
ae493fa
Wrap mining fee text function in hook
httpiga Feb 18, 2023
344e18d
Wrap estimated max collaborator fee in hook
httpiga Feb 19, 2023
4a1dcb5
Get real fee values
httpiga Feb 19, 2023
69a961e
Fix TS error "TFunction is not generic"
httpiga Feb 26, 2023
4d0327d
Improve clarity
httpiga May 27, 2023
32ba78a
Fix popover header visibility in dark mode
httpiga Jun 5, 2023
4e1f69d
Use passive voice in "cannot estimate mining fee" text
httpiga Jun 5, 2023
4a6f6fb
Fix past usage in "cannot estimate mining fee" text
httpiga Jun 5, 2023
bc8d276
Fix popover dark mode colors
httpiga Jun 12, 2023
5ce4912
Align UI to the new proposal from editwentyone
httpiga Aug 12, 2023
52b9d90
ui: de-emphasize non-effective fee card content
theborakompanioni Aug 23, 2023
476b586
chore: fix browser warning for fee breakdown subtitle
theborakompanioni Aug 23, 2023
40b0a72
chore(wording): Fee Estimation -> Maximum collaborator fee limit
theborakompanioni Aug 23, 2023
de80e6a
chore: use css class text-small for fee breakdown labels
theborakompanioni Aug 23, 2023
d11afe6
chore: use useMemo in FeeBreakdown component
theborakompanioni Aug 23, 2023
9cf74c5
feat(FeeBreakdown): open fee modal on click
theborakompanioni Aug 23, 2023
98bdcc1
ui(FeeBreakdown): add less-or-equal sign
theborakompanioni Aug 23, 2023
33c9a03
ui(FeeBreakdown): show max collaborator fee value in title
theborakompanioni Aug 23, 2023
0146241
ui(FeeBreakdown): open cj fee section by default
theborakompanioni Aug 23, 2023
054bc22
ui(FeeSettings): switch position of mining and collaborator fees
theborakompanioni Aug 24, 2023
86c9846
ui(FeeBreakdown): now works with sweep transactions
theborakompanioni Aug 24, 2023
bc901c7
ui(FeeBreakdown): market maker fees -> collaborator fees
theborakompanioni Aug 24, 2023
4dbe975
fix(actions): temporarily print linter debug messages
theborakompanioni Aug 24, 2023
0c3dc38
build(deps): update to prettier v2.8.7
theborakompanioni Aug 24, 2023
c7f9b0d
ui(FeeBreakdown): emphasize collaborator fee note
theborakompanioni Aug 24, 2023
5432b6d
fix(FeeBreakdown): improve handling of missing config values
theborakompanioni Aug 25, 2023
6f8efef
chore(FeeBreakdown): Remove unused `AccordionInfo`
httpiga Aug 28, 2023
b331c58
revert: temporarily print linter debug messages
httpiga Aug 28, 2023
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
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"jest-watch-typeahead": "^0.6.5",
"jest-websocket-mock": "^2.4.0",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"prettier": "^2.8.7",
"react-scripts": "^5.0.1",
"typescript": "^4.8.4"
},
Expand Down
4 changes: 0 additions & 4 deletions src/components/Jam.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@
text-decoration: none;
color: var(--bs-body-color);
}

.small-text {
font-size: 0.8rem;
}
4 changes: 1 addition & 3 deletions src/components/Jam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,7 @@ export default function Jam({ wallet }: JamProps) {
<Sprite symbol="checkmark" width="25" height="25" className="text-secondary" />
<div className="d-flex flex-column">
<div>{t('scheduler.complete_wallet_title')}</div>
<div className={`text-secondary ${styles['small-text']}`}>
{t('scheduler.complete_wallet_subtitle')}
</div>
<div className="text-secondary text-small">{t('scheduler.complete_wallet_subtitle')}</div>
</div>
</div>
<>
Expand Down
90 changes: 45 additions & 45 deletions src/components/PaymentConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,15 @@ import * as rb from 'react-bootstrap'
import Sprite from './Sprite'
import Balance from './Balance'
import { useSettings } from '../context/SettingsContext'
import { estimateMaxCollaboratorFee, FeeValues, toTxFeeValueUnit } from '../hooks/Fees'

import { isValidNumber } from '../utils'
import { FeeValues, useEstimatedMaxCollaboratorFee, toTxFeeValueUnit } from '../hooks/Fees'
import { ConfirmModal, ConfirmModalProps } from './Modal'

import styles from './PaymentConfirmModal.module.css'
import { AmountSats } from '../libs/JmWalletApi'
import { jarInitial } from './jars/Jar'
import { isValidNumber } from '../utils'

interface PaymentDisplayInfo {
sourceJarIndex?: JarIndex
destination: String
amount: AmountSats
isSweep: boolean
isCoinjoin: boolean
numCollaborators?: number
feeConfigValues?: FeeValues
showPrivacyInfo?: boolean
}

interface PaymentConfirmModalProps extends ConfirmModalProps {
data: PaymentDisplayInfo
}

export function PaymentConfirmModal({
data: {
sourceJarIndex,
destination,
amount,
isSweep,
isCoinjoin,
numCollaborators,
feeConfigValues,
showPrivacyInfo = true,
},
...confirmModalProps
}: PaymentConfirmModalProps) {
const useMiningFeeText = ({ feeConfigValues }: { feeConfigValues?: FeeValues }) => {
const { t } = useTranslation()
const settings = useSettings()

const estimatedMaxCollaboratorFee = useMemo(() => {
if (!isCoinjoin || !feeConfigValues) return null
if (!isValidNumber(amount) || !isValidNumber(numCollaborators)) return null
if (!isValidNumber(feeConfigValues.max_cj_fee_abs) || !isValidNumber(feeConfigValues.max_cj_fee_rel)) return null
return estimateMaxCollaboratorFee({
amount,
collaborators: numCollaborators!,
maxFeeAbs: feeConfigValues.max_cj_fee_abs!,
maxFeeRel: feeConfigValues.max_cj_fee_rel!,
})
}, [amount, isCoinjoin, numCollaborators, feeConfigValues])

const miningFeeText = useMemo(() => {
if (!feeConfigValues) return null
Expand Down Expand Up @@ -89,6 +47,48 @@ export function PaymentConfirmModal({
}
}, [t, feeConfigValues])

return miningFeeText
}

interface PaymentDisplayInfo {
sourceJarIndex?: JarIndex
destination: String
amount: AmountSats
isSweep: boolean
isCoinjoin: boolean
numCollaborators?: number
feeConfigValues?: FeeValues
showPrivacyInfo?: boolean
}

interface PaymentConfirmModalProps extends ConfirmModalProps {
data: PaymentDisplayInfo
}

export function PaymentConfirmModal({
data: {
sourceJarIndex,
destination,
amount,
isSweep,
isCoinjoin,
numCollaborators,
feeConfigValues,
showPrivacyInfo = true,
},
...confirmModalProps
}: PaymentConfirmModalProps) {
const { t } = useTranslation()
const settings = useSettings()

const miningFeeText = useMiningFeeText({ feeConfigValues })
const estimatedMaxCollaboratorFee = useEstimatedMaxCollaboratorFee({
isCoinjoin,
feeConfigValues,
amount,
numCollaborators: numCollaborators || null,
})

return (
<ConfirmModal {...confirmModalProps}>
<rb.Container className="mt-2">
Expand Down
2 changes: 1 addition & 1 deletion src/components/ScheduleProgress.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const ScheduleProgress = ({ schedule }) => {
</Trans>
)}
</p>
<p className={['text-secondary', styles['text-small']].join(' ')}>{t('scheduler.progress_description')}</p>
<p className="text-secondary text-small">{t('scheduler.progress_description')}</p>
</div>
<div className={styles['schedule-progress']}>
<div className={styles['progress-container']}>
Expand Down
4 changes: 0 additions & 4 deletions src/components/ScheduleProgress.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,3 @@
width: 100%;
}
}

.text-small {
font-size: 0.8rem;
}
172 changes: 172 additions & 0 deletions src/components/Send/FeeBreakdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { useMemo, PropsWithChildren } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import classNames from 'classnames'
import Balance from '../Balance'
import * as rb from 'react-bootstrap'
import { useSettings } from '../../context/SettingsContext'
import { SATS, formatSats, factorToPercentage } from '../../utils'
import { FeeValues } from '../../hooks/Fees'
import { AmountSats } from '../../libs/JmWalletApi'

interface FeeBreakdownProps {
feeConfigValues?: FeeValues
numCollaborators: number | null
amount: number | null
onClick?: () => void
}

type FeeCardProps = {
amount: AmountSats | null
feeConfigValue: number | undefined
highlight: boolean
subtitle?: React.ReactNode
onClick?: () => void
}
const FeeCard = ({ amount, feeConfigValue, highlight, subtitle, onClick }: FeeCardProps) => {
const settings = useSettings()
const { t } = useTranslation()

return (
<rb.Card onClick={onClick} border={highlight ? (settings.theme === 'dark' ? 'light' : 'dark') : undefined}>
<rb.Card.Body
className={classNames('text-center py-2', {
'text-muted': !highlight,
})}
>
<div className="fs-5">
{feeConfigValue === undefined ? (
t('send.fee_breakdown.placeholder_config_value_not_present')
) : (
<>
{amount === null ? (
t('send.fee_breakdown.placeholder_amount_missing_amount')
) : (
<>
&le;
<Balance convertToUnit={SATS} valueString={amount.toString()} showBalance={true} />
</>
)}
</>
)}
</div>
<div className="text-secondary text-small">{subtitle}</div>
</rb.Card.Body>
</rb.Card>
)
}

const FeeBreakdown = ({
feeConfigValues,
numCollaborators,
amount,
onClick = () => {},
}: PropsWithChildren<FeeBreakdownProps>) => {
const { t } = useTranslation()

/** eg: "0.03%" */
const maxSettingsRelativeFee = useMemo(
() =>
feeConfigValues?.max_cj_fee_rel
? `${factorToPercentage(feeConfigValues.max_cj_fee_rel)}%`
: t('send.fee_breakdown.placeholder_config_value_not_present'),
[feeConfigValues, t]
)

/** eg: 44658 (expressed in sats) */
const maxEstimatedRelativeFee = useMemo(
() =>
feeConfigValues?.max_cj_fee_rel && numCollaborators && amount && amount > 0
? Math.ceil(amount * feeConfigValues.max_cj_fee_rel) * numCollaborators
: null,
[feeConfigValues, amount, numCollaborators]
)

/** eg: "8,636 sats" */
const maxSettingsAbsoluteFee = useMemo(
() =>
feeConfigValues?.max_cj_fee_abs
? `${formatSats(feeConfigValues.max_cj_fee_abs)} sats`
: t('send.fee_breakdown.placeholder_config_value_not_present'),
[feeConfigValues, t]
)

/** eg: 77724 (expressed in sats) */
const maxEstimatedAbsoluteFee = useMemo(
() =>
feeConfigValues?.max_cj_fee_abs && numCollaborators ? feeConfigValues.max_cj_fee_abs * numCollaborators : null,
[feeConfigValues, numCollaborators]
)

const isAbsoluteFeeHighlighted = useMemo(
() =>
maxEstimatedAbsoluteFee && maxEstimatedRelativeFee ? maxEstimatedAbsoluteFee > maxEstimatedRelativeFee : false,
[maxEstimatedAbsoluteFee, maxEstimatedRelativeFee]
)

const isRelativeFeeHighlighted = useMemo(
() =>
maxEstimatedAbsoluteFee && maxEstimatedRelativeFee ? maxEstimatedRelativeFee > maxEstimatedAbsoluteFee : false,
[maxEstimatedAbsoluteFee, maxEstimatedRelativeFee]
)

return (
<rb.Row className="mb-2">
<rb.Col>
<rb.Form.Label
className={classNames('mb-1', 'text-small', {
'text-muted': !isAbsoluteFeeHighlighted,
})}
>
{t('send.fee_breakdown.absolute_limit')}
</rb.Form.Label>
<FeeCard
highlight={isAbsoluteFeeHighlighted}
amount={maxEstimatedAbsoluteFee}
feeConfigValue={feeConfigValues?.max_cj_fee_abs}
subtitle={
<Trans
i18nKey="send.fee_breakdown.fee_card_subtitle"
components={{
1: <span className="text-decoration-underline link-secondary" />,
}}
values={{
numCollaborators,
maxFee: maxSettingsAbsoluteFee,
}}
/>
}
onClick={onClick}
/>
</rb.Col>
<rb.Col>
<rb.Form.Label
className={classNames('mb-1', 'text-small', {
'text-muted': !isRelativeFeeHighlighted,
})}
>
{t('send.fee_breakdown.relative_limit')}
</rb.Form.Label>
<FeeCard
highlight={isRelativeFeeHighlighted}
amount={maxEstimatedRelativeFee}
feeConfigValue={feeConfigValues?.max_cj_fee_rel}
subtitle={
<Trans
i18nKey="send.fee_breakdown.fee_card_subtitle"
components={{
1: <span className="text-decoration-underline link-secondary" />,
}}
values={{
numCollaborators,
maxFee: maxSettingsRelativeFee,
}}
/>
}
onClick={onClick}
/>
</rb.Col>
</rb.Row>
)
}

export default FeeBreakdown
Loading