Skip to content

Commit

Permalink
✨ BorrowAtMaturity: installments
Browse files Browse the repository at this point in the history
  • Loading branch information
sebipap committed Mar 16, 2024
1 parent 03e67b6 commit 4221bb9
Show file tree
Hide file tree
Showing 19 changed files with 521 additions and 300 deletions.
39 changes: 23 additions & 16 deletions components/OperationsModal/BorrowDateSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Box, Typography } from '@mui/material';

import DropdownMenu from 'components/DropdownMenu';
import parseTimestamp from 'utils/parseTimestamp';

import { useTranslation } from 'react-i18next';
import parseTimestamp from 'utils/parseTimestamp';
import { useOperationContext } from 'contexts/OperationContext';
import ModalAlert from 'components/common/modal/ModalAlert';
import DropdownMenu from 'components/DropdownMenu';
import getHourUTC2Local from 'utils/getHourUTC2Local';
import { track } from 'utils/mixpanel';

import ModalAlert from '../../common/modal/ModalAlert';
import { DAY } from 'utils/utils';

type DateOptionProps = {
label: string;
Expand All @@ -32,6 +30,11 @@ export function DateOption({ label, option = false }: DateOptionProps) {
function BorrowDateSelector() {
const { t } = useTranslation();
const { date, dates, setDate } = useOperationContext();
const daysToMaturity = useMemo(() => {
if (!date) return;
const now = BigInt(Math.round(Date.now() / 1000));
return Number((date - now) / DAY);
}, [date]);

const handleChange = useCallback(
(maturity: bigint) => {
Expand All @@ -46,8 +49,6 @@ function BorrowDateSelector() {
[date, setDate],
);

const daysToMaturity = Math.floor((Number(date) - Date.now() / 1000) / 60 / 60 / 24);

return (
<Box display="flex" flexDirection="column" flexGrow={1} gap="20px">
<Box display="flex" justifyContent="space-between" alignItems="center">
Expand All @@ -62,17 +63,23 @@ function BorrowDateSelector() {
renderValue={date ? <DateOption label={parseTimestamp(date)} /> : null}
renderOption={(o: bigint) => <DateOption option label={parseTimestamp(o)} />}
/>
<Typography color="figma.grey.500" fontWeight={500} fontSize={13} fontFamily="fontFamilyMonospaced">
{t('at {{hour}}', { hour: getHourUTC2Local() })}
<Typography
color="figma.grey.500"
fontWeight={500}
fontSize={13}
fontFamily="fontFamilyMonospaced"
textAlign="right"
marginRight="28.4px"
>
{t('at {{hour}}', { hour: getHourUTC2Local(undefined, 'h a [UTC] Z') })}
</Typography>
</Box>
</Box>
{daysToMaturity <= 7 && (
{daysToMaturity && daysToMaturity <= 7 && (
<ModalAlert
message={t(
'Dues in {{daysToMaturity}} days. for optimal benefits, consider selecting a pool with a longer remaining duration.',
{ daysToMaturity },
)}
message={`${
daysToMaturity === 1 ? t('Dues in 1 day.') : t('Dues in {{daysToMaturity}} days.', { daysToMaturity })
} ${t('For optimal benefits, consider selecting a pool with a longer remaining duration.')}`}
variant="warning"
/>
)}
Expand Down
11 changes: 9 additions & 2 deletions components/OperationsModal/ModalGif/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Props = {
function ModalGif({ tx, tryAgain }: Props) {
const { t } = useTranslation();
const translateOperation = useTranslateOperation();
const { operation, symbol, qty, date } = useOperationContext();
const { operation, symbol, qty, date, installments } = useOperationContext();

const isLoading = useMemo(() => tx.status === 'processing' || tx.status === 'loading', [tx]);
const isSuccess = useMemo(() => tx.status === 'success', [tx]);
Expand Down Expand Up @@ -78,7 +78,14 @@ function ModalGif({ tx, tryAgain }: Props) {
pastAction: translateOperation(operation, { variant: 'past' }),
qty,
symbol: formatSymbol(symbol),
}) + (reminder && date ? t(' until {{daysLeft}}', { daysLeft: parseTimestamp(date) }) : '')}
}) +
(reminder && date
? installments > 1
? t(' In {{installments}} installments', {
installments,
})
: t(' until {{daysLeft}}', { daysLeft: parseTimestamp(date) })
: '')}
{isError && t('Something went wrong')}
</Typography>
<Box display="flex" flexDirection="column" alignItems="center" gap="8px" pt={1}>
Expand Down
6 changes: 5 additions & 1 deletion components/Reminder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import parseTimestamp from 'utils/parseTimestamp';
import useTranslateOperation from 'hooks/useTranslateOperation';
import type { Operation } from 'types/Operation';
import { track } from 'utils/mixpanel';
import { MATURITY_DAYS } from 'utils/utils';
import { useOperationContext } from 'contexts/OperationContext';

type Props = {
operation: Operation;
Expand All @@ -18,6 +20,7 @@ const Reminder: FC<Props> = ({ operation, maturity }) => {
const translateOperation = useTranslateOperation();
const { palette } = useTheme();
const buttonRef = useRef<HTMLInputElement>(null);
const { installments } = useOperationContext();

const isBorrow = useMemo(() => operation?.startsWith('borrow'), [operation]);

Expand All @@ -33,6 +36,7 @@ const Reminder: FC<Props> = ({ operation, maturity }) => {
options: ['Google', 'Apple', 'iCal', 'Microsoft365', 'MicrosoftTeams', 'Outlook.com', 'Yahoo'],
timeZone: 'UTC',
lightMode: palette.mode,
recurrence: `RRULE:FREQ=DAILY;INTERVAL=${MATURITY_DAYS.toString()};COUNT=${installments}`,
};
track('Button Clicked', {
location: 'Reminder',
Expand All @@ -42,7 +46,7 @@ const Reminder: FC<Props> = ({ operation, maturity }) => {
});

if (buttonRef.current) atcb_action(config, buttonRef.current);
}, [maturity, operation, palette.mode, t, translateOperation]);
}, [installments, maturity, operation, palette.mode, t, translateOperation]);

return (
<Box
Expand Down
2 changes: 1 addition & 1 deletion components/markets/MarketsTables/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,10 @@ const MarketTables: FC = () => {
bestFixedBorrow.rate < Number(formatEther(floatingBorrowRate))
? { maturity: bestFixedBorrow.maturity, rate: bestFixedBorrow.rate }
: { maturity: 0n, rate: Number(formatEther(floatingBorrowRate)) };

const [first, second] = [...fixedPools].sort((a, b) => Number(a.maturity) - Number(b.maturity));
const now = BigInt(Math.round(Date.now() / 1000));
const upcomingMaturity = first.maturity - now < WEEK ? second.maturity : first.maturity;

tempRows.push({
symbol,
totalDeposited: formatNumber(formatUnits((totalDeposited * usdPrice) / WEI_PER_ETHER, decimals)),
Expand Down
57 changes: 57 additions & 0 deletions components/operations/BorrowAtMaturity/Installments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useOperationContext } from 'contexts/OperationContext';
import { Box, Typography } from '@mui/material';
import { MATURITY_DAYS } from 'utils/utils';
import ModalAlert from 'components/common/modal/ModalAlert';
import InstallmentsOptions from './InstallmentsOptions';

export default function Installments({ setBreakdownSheetOpen }: { setBreakdownSheetOpen: (open: boolean) => void }) {
const { t } = useTranslation();
const { installments } = useOperationContext();

const viewBreakdown = useCallback(() => {
setBreakdownSheetOpen(true);
}, [setBreakdownSheetOpen]);

return (
<Box display="flex" flexDirection="column" gap={2.5} flex={1}>
<Box display="flex" justifyContent="space-between">
<Box display="flex" gap={1} alignItems="center">
<Typography
fontSize={11}
fontWeight={700}
color="white"
sx={{
background: ({ palette }) => palette.green,
borderRadius: '4px',
px: 0.5,
textTransform: 'uppercase',
}}
>
{t('New!')}
</Typography>
<Typography fontFamily="fontFamilyMonospaced" color="grey.600" fontSize={12} fontWeight={500} noWrap>
{t('Installments')}
</Typography>
</Box>
<InstallmentsOptions />
</Box>
{installments >= 2 && (
<ModalAlert
variant="info"
message={
<>
{t('Each installment is due every {{interval}} days.', {
interval: MATURITY_DAYS.toString(),
})}{' '}
<a style={{ textDecorationLine: 'underline', cursor: 'pointer' }} onClick={viewBreakdown}>
{t('Payment schedule')}
</a>
</>
}
/>
)}
</Box>
);
}
57 changes: 57 additions & 0 deletions components/operations/BorrowAtMaturity/InstallmentsBreakdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Box, Button, Skeleton, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useOperationContext } from 'contexts/OperationContext';
import TableHeadCell from 'components/common/TableHeadCell';
import parseTimestamp from 'utils/parseTimestamp';
import useAccountData from 'hooks/useAccountData';

export default function InstallmentsBreakdown({ onClose }: { onClose: () => void }) {
const { t } = useTranslation();
const { installmentsDetails, symbol } = useOperationContext();
const { marketAccount } = useAccountData(symbol);
if (!installmentsDetails || !marketAccount) return <Skeleton />;

return (
<Box display="flex" flexDirection="column" flex={1}>
<Box
sx={{
border: '1px solid #E3E5E8',
px: 2,
borderRadius: 1,
maxHeight: 392,
overflowY: 'scroll',
'& tr:last-child td': {
border: 'none',
},
}}
>
<Table>
<TableHead>
<TableRow>
<TableHeadCell title={''} />
<TableHeadCell title={t('Maturity')} />
</TableRow>
</TableHead>
<TableBody>
{installmentsDetails.installmentsRepayAmount.map((option, index) => (
<TableRow key={option.toString()}>
<TableCell>
<Typography fontSize={19} fontWeight={500} color="grey.500">
{index + 1}
</Typography>
</TableCell>
<TableCell>
<Typography fontWeight={'bold'}>{parseTimestamp(installmentsDetails.maturities[index])}</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
<Button onClick={onClose} variant="contained" sx={{ marginTop: 'auto' }}>
{t('Back')}
</Button>
</Box>
);
}
88 changes: 88 additions & 0 deletions components/operations/BorrowAtMaturity/InstallmentsOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useCallback, useMemo } from 'react';
import DropdownMenu from 'components/DropdownMenu';
import formatNumber from 'utils/formatNumber';
import { useOperationContext } from 'contexts/OperationContext';
import useAccountData from 'hooks/useAccountData';
import { formatUnits } from 'viem';
import { Box, Skeleton, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';

type OptionProps = {
installments: number;
repayAmount: bigint;
option?: boolean;
};

export function Option({ installments, repayAmount, option = false }: OptionProps) {
const { symbol } = useOperationContext();
const { marketAccount } = useAccountData(symbol);

return (
<Typography
fontWeight={600}
fontSize={option ? 14 : 18}
mt={option ? '4px' : '6px'}
data-testid={!option ? 'modal-date-selector' : undefined}
sx={{
'& span': {
color: 'grey.600',
},
}}
>
{installments} x{' '}
<span>
{repayAmount === 0n ? '' : '~'}
{marketAccount ? formatNumber(formatUnits(repayAmount, marketAccount.decimals)) : <Skeleton />}
</span>
</Typography>
);
}

export default function InstallmentsOptions() {
const { t } = useTranslation();
const { installments, onInstallmentsChange, installmentsOptions, symbol } = useOperationContext();
const { marketAccount } = useAccountData(symbol);

const handleChange = useCallback(
(option: { installments: number }) => {
onInstallmentsChange(option.installments);
},
[onInstallmentsChange],
);

const option = useMemo(() => {
if (!installmentsOptions) return;
return installmentsOptions.find((o) => Number(o.installments) === installments);
}, [installments, installmentsOptions]);

if (!installmentsOptions || !option) return <Skeleton />;

return (
<Box display="flex" flexDirection="column" alignItems="end">
<DropdownMenu
label={t('Maturity')}
options={installmentsOptions}
onChange={handleChange}
renderValue={option ? <Option {...option} /> : null}
renderOption={(o: OptionProps) => <Option option {...o} />}
/>
{option ? (
<Typography
color="figma.grey.500"
fontWeight={500}
fontSize={13}
fontFamily="fontFamilyMonospaced"
textAlign="right"
marginRight="28.4px"
>
{t('Total')}{' '}
{option && marketAccount ? (
formatNumber(formatUnits(option.repayAmount * BigInt(option.installments), marketAccount.decimals))
) : (
<Skeleton />
)}
</Typography>
) : null}
</Box>
);
}
Loading

0 comments on commit 4221bb9

Please sign in to comment.