Skip to content

Commit

Permalink
feat: Add decimal accuracy to repaying, use new loading, success & er…
Browse files Browse the repository at this point in the history
…ror dialogs.
  • Loading branch information
kovipu committed Feb 11, 2025
1 parent 20eecd8 commit cc18eb2
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 126 deletions.
89 changes: 8 additions & 81 deletions src/components/LoansModal/LoansModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Success } from '@components/Alert';
import { CircleButton } from '@components/Button';
import { Dialog } from '@components/Dialog';
import type { Loan } from '@contexts/loan-context';
import type { SupportedCurrency } from 'currencies';
import { isNil } from 'ramda';
import { useState } from 'react';
import { IoClose as CloseIcon } from 'react-icons/io5';
import LoansView from './LoansView';
import RepayView from './RepayView';

Expand All @@ -13,99 +10,29 @@ export interface LoansModalProps {
onClose: () => void;
}

type RepayAlert =
| {
kind: 'success';
ticker: SupportedCurrency;
amount: string;
}
| {
kind: 'full-success';
ticker: SupportedCurrency;
};

const LoansModal = ({ modalId, onClose }: LoansModalProps) => {
const [selectedLoan, setSelectedLoan] = useState<Loan | null>(null);
const [alert, setAlert] = useState<RepayAlert | null>(null);

const handleBackClicked = () => setSelectedLoan(null);

const handleClose = () => {
setSelectedLoan(null);
setAlert(null);
onClose();
};

const handleRepaySuccess = (ticker: SupportedCurrency, amount: string) => {
setSelectedLoan(null);
setAlert({ kind: 'success', ticker, amount });
};

const handleRepayFullSuccess = (ticker: SupportedCurrency) => {
setSelectedLoan(null);
setAlert({ kind: 'full-success', ticker });
};

const handleAlertClose = () => setAlert(null);

const handleRepayClicked = (loan: Loan) => {
setAlert(null);
setSelectedLoan(loan);
};

return (
<dialog id={modalId} className="modal">
<div className="modal-box w-full max-w-full md:w-[800px] flex flex-col">
{alert && alert.kind === 'success' ? (
<RepaySuccessAlert onClose={handleAlertClose} ticker={alert.ticker} amount={alert.amount} />
) : null}
{alert && alert.kind === 'full-success' ? (
<FullRepaySuccessAlert onClose={handleAlertClose} ticker={alert.ticker} />
) : null}
{isNil(selectedLoan) ? (
<LoansView onClose={handleClose} onRepay={handleRepayClicked} />
) : (
<RepayView
loan={selectedLoan}
onBack={handleBackClicked}
onSuccess={handleRepaySuccess}
onFullSuccess={handleRepayFullSuccess}
/>
)}
</div>
<form method="dialog" className="modal-backdrop">
<button type="button" onClick={handleClose}>
close
</button>
</form>
</dialog>
<Dialog modalId={modalId} onClose={handleClose} className="min-w-96">
{isNil(selectedLoan) ? (
<LoansView onClose={handleClose} onRepay={handleRepayClicked} />
) : (
<RepayView loan={selectedLoan} onBack={handleBackClicked} />
)}
</Dialog>
);
};

type RepaySuccessAlertProps = {
ticker: SupportedCurrency;
amount: string;
onClose: () => void;
};

const RepaySuccessAlert = ({ ticker, amount, onClose }: RepaySuccessAlertProps) => (
<Success className="mb-8">
<span>
Successfully repaid {amount} {ticker}
</span>
<CircleButton onClick={onClose} variant="ghost-dark">
<CloseIcon size="1.4rem" />
</CircleButton>
</Success>
);

const FullRepaySuccessAlert = ({ ticker, onClose }: { ticker: SupportedCurrency; onClose: VoidFunction }) => (
<Success className="mb-8">
<span>Successfully repaid all of the borrowed {ticker}</span>
<CircleButton onClick={onClose} variant="ghost-dark">
<CloseIcon size="1.4rem" />
</CircleButton>
</Success>
);

export default LoansModal;
105 changes: 60 additions & 45 deletions src/components/LoansModal/RepayView.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,56 @@
import { Button } from '@components/Button';
import { CryptoAmountSelector } from '@components/CryptoAmountSelector';
import { Loading } from '@components/Loading';
import { ErrorDialogContent, LoadingDialogContent, SuccessDialogContent } from '@components/Dialog';
import { type Loan, useLoans } from '@contexts/loan-context';
import { usePools } from '@contexts/pool-context';
import { useWallet } from '@contexts/wallet-context';
import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { SCALAR_7, formatAPR, formatAmount, toCents } from '@lib/formatting';
import type { SupportedCurrency } from 'currencies';
import { type ChangeEvent, useState } from 'react';
import { formatAPR, formatAmount, toCents } from '@lib/formatting';
import { useState } from 'react';
import { CURRENCY_BINDINGS } from 'src/currency-bindings';

interface RepayViewProps {
loan: Loan;
onBack: () => void;
onSuccess: (ticker: SupportedCurrency, amount: string) => void;
onFullSuccess: (ticker: SupportedCurrency) => void;
onBack: VoidFunction;
}

const RepayView = ({ loan, onBack, onSuccess, onFullSuccess }: RepayViewProps) => {
const RepayView = ({ loan, onBack }: RepayViewProps) => {
const { borrowedAmount, borrowedTicker, collateralAmount, collateralTicker, unpaidInterest } = loan;
const { name } = CURRENCY_BINDINGS[borrowedTicker];
const { wallet, signTransaction, refetchBalances } = useWallet();
const { prices, pools } = usePools();
const { refetchLoans } = useLoans();
const [amount, setAmount] = useState('0');
const [amount, setAmount] = useState(0n);
const [isRepaying, setIsRepaying] = useState(false);
const [isRepayingAll, setIsRepayingAll] = useState(false);
const [success, setSuccess] = useState<'PARTIAL_REPAY_SUCCESS' | 'FULL_REPAY_SUCCESS' | null>(null);
const [error, setError] = useState<Error | null>(null);

const loanBalance = borrowedAmount + unpaidInterest;
const apr = pools?.[borrowedTicker]?.annualInterestRate;
const price = prices?.[borrowedTicker];
const valueCents = price ? toCents(price, BigInt(amount) * SCALAR_7) : undefined;
const valueCents = price ? toCents(price, amount) : undefined;

const max = (loanBalance / SCALAR_7).toString();

const handleAmountChange = (ev: ChangeEvent<HTMLInputElement>) => {
setAmount(ev.target.value);
const handleAmountChange = (stroops: bigint) => {
setAmount(stroops);
};

const handleSelectMax = () => {
setAmount(max);
setAmount(loanBalance);
};

const handleRepayClick = async () => {
if (!wallet) return;

setIsRepaying(true);

const tx = await loanManagerClient.repay({ user: wallet.address, amount: BigInt(amount) * SCALAR_7 });
const tx = await loanManagerClient.repay({ user: wallet.address, amount });
try {
await tx.signAndSend({ signTransaction });
onSuccess(borrowedTicker, amount);
setSuccess('PARTIAL_REPAY_SUCCESS');
} catch (err) {
console.error('Error repaying', err);
alert('Error repaying');
setError(err as Error);
}
refetchLoans();
refetchBalances();
Expand All @@ -68,22 +65,57 @@ const RepayView = ({ loan, onBack, onSuccess, onFullSuccess }: RepayViewProps) =
const tx = await loanManagerClient.repay_and_close_manager({
user: wallet.address,
// +5% to liabilities. TEMPORARY hard-coded solution for max allowance.
max_allowed_amount: (BigInt(loanBalance) * BigInt(5)) / BigInt(100) + BigInt(loanBalance),
max_allowed_amount: (loanBalance * 5n) / 100n + loanBalance,
});
try {
await tx.signAndSend({ signTransaction });
onFullSuccess(borrowedTicker);
setSuccess('FULL_REPAY_SUCCESS');
} catch (err) {
console.error('Error repaying', err);
alert('Error repaying');
setError(err as Error);
}
refetchLoans();
refetchBalances();
setIsRepayingAll(false);
};

if (isRepaying) {
return (
<LoadingDialogContent
title="Repaying"
subtitle={`Repaying ${formatAmount(amount)} ${borrowedTicker}.`}
buttonText="Back"
onClick={onBack}
/>
);
}

if (isRepayingAll) {
return (
<LoadingDialogContent
title="Repaying"
subtitle={`Repaying ${formatAmount(loanBalance)} ${borrowedTicker}, all of your outstanding loan.`}
buttonText="Back"
onClick={onBack}
/>
);
}

if (success) {
const subtitle =
success === 'FULL_REPAY_SUCCESS'
? `Successfully repaid all of your loan. The collateral ${formatAmount(collateralAmount)} ${collateralTicker} was returned back to your wallet.`
: `Successfully repaid ${formatAmount(amount)} ${borrowedTicker}.`;

return <SuccessDialogContent subtitle={subtitle} buttonText="Back" onClick={onBack} />;
}

if (error) {
return <ErrorDialogContent error={error} onClick={onBack} />;
}

return (
<>
<div className="md:w-[700px]">
<h3 className="text-xl font-bold tracking-tight">Repay {name}</h3>
<p className="my-4">
Repay some or all of your loan. Repaying the loan in full will return the collateral back to you.
Expand All @@ -97,7 +129,7 @@ const RepayView = ({ loan, onBack, onSuccess, onFullSuccess }: RepayViewProps) =
</p>
<p className="font-bold mb-2 mt-6">Select the amount to repay</p>
<CryptoAmountSelector
max={max}
max={loanBalance}
value={amount}
valueCents={valueCents}
ticker={borrowedTicker}
Expand All @@ -108,30 +140,13 @@ const RepayView = ({ loan, onBack, onSuccess, onFullSuccess }: RepayViewProps) =
<Button onClick={onBack} variant="ghost">
Back
</Button>
{!isRepaying ? (
<Button disabled={isRepayingAll || amount === '0'} onClick={handleRepayClick}>
Repay
</Button>
) : (
<LoadingButton />
)}
{!isRepayingAll ? (
<Button disabled={isRepaying} onClick={handleRepayAllClick}>
Repay All
</Button>
) : (
<LoadingButton />
)}
<Button disabled={amount === 0n} onClick={handleRepayClick}>
Repay
</Button>
<Button onClick={handleRepayAllClick}>Repay All</Button>
</div>
</>
</div>
);
};

const LoadingButton = () => (
<Button disabled>
<Loading />
Repaying
</Button>
);

export default RepayView;

0 comments on commit cc18eb2

Please sign in to comment.