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

implement offramp summary popup #394

7 changes: 2 additions & 5 deletions src/components/FeeCollapse/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FC } from 'preact/compat';
import Big from 'big.js';
import { roundDownToTwoDecimals } from '../../helpers/parseNumbers';
import { OutputTokenDetails } from '../../constants/tokenConfig';
import { useEventsContext } from '../../contexts/events';
import { useOfframpFees } from '../../hooks/useOfframpFees';

export function calculateTotalReceive(toAmount: Big, outputToken: OutputTokenDetails): string {
const feeBasisPoints = outputToken.offrampFeesBasisPoints;
Expand Down Expand Up @@ -32,10 +32,7 @@ export const FeeCollapse: FC<CollapseProps> = ({ toAmount = Big(0), toToken, exc
trackEvent({ event: 'click_details' });
};

const toAmountFixed = roundDownToTwoDecimals(toAmount);
const totalReceive = calculateTotalReceive(toAmount, toToken);
const totalReceiveFormatted = roundDownToTwoDecimals(Big(totalReceive));
const feesCost = roundDownToTwoDecimals(Big(toAmountFixed || 0).sub(totalReceive));
const { toAmountFixed, totalReceiveFormatted, feesCost } = useOfframpFees(toAmount, toToken);

return (
<div className="border border-blue-700 collapse-arrow collapse" onClick={trackFeeCollapseOpen}>
Expand Down
155 changes: 155 additions & 0 deletions src/components/OfframpSummaryDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { ArrowDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
import { FC, useState } from 'preact/compat';
import Big from 'big.js';

import { InputTokenDetails, OutputTokenDetails } from '../../constants/tokenConfig';
import { UseTokenOutAmountResult } from '../../hooks/nabla/useTokenAmountOut';
import { useGetAssetIcon } from '../../hooks/useGetAssetIcon';
import { useOfframpFees } from '../../hooks/useOfframpFees';
import { useNetwork } from '../../contexts/network';
import { Networks } from '../../helpers/networks';

import { ExchangeRate } from '../ExchangeRate';
import { NetworkIcon } from '../NetworkIcon';
import { Dialog } from '../Dialog';
import { Spinner } from '../Spinner';

interface AssetDisplayProps {
amount: string;
symbol: string;
iconSrc: string;
iconAlt: string;
}

const AssetDisplay = ({ amount, symbol, iconSrc, iconAlt }: AssetDisplayProps) => (
<div className="flex items-center justify-between w-full">
<span className="text-lg font-bold">
{amount} {symbol}
</span>
<img src={iconSrc} alt={iconAlt} className="w-8 h-8" />
</div>
);

interface FeeDetailsProps {
network: Networks;
feesCost: string;
fiatSymbol: string;
tokenOutAmount: UseTokenOutAmountResult;
fromToken: InputTokenDetails;
toToken: OutputTokenDetails;
}

const FeeDetails = ({ network, feesCost, fiatSymbol, tokenOutAmount, fromToken, toToken }: FeeDetailsProps) => (
<section className="mt-6">
<div className="flex justify-between mb-2">
<p>
Offramp fee ({`${toToken.offrampFeesBasisPoints / 100}%`}
{toToken.offrampFeesFixedComponent ? ` + ${toToken.offrampFeesFixedComponent} ${fiatSymbol}` : ''})
</p>
<p className="flex items-center gap-2">
<NetworkIcon network={network} className="w-4 h-4" />
<strong>
{feesCost} {fiatSymbol}
</strong>
</p>
</div>
<div className="flex justify-between mb-2">
<p>Quote</p>
<p>
<strong>
<ExchangeRate tokenOutData={tokenOutAmount} fromToken={fromToken} toTokenSymbol={fiatSymbol} />
</strong>
</p>
</div>
<div className="flex justify-between">
<p>Partner</p>
<a
href={toToken.anchorHomepageUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{toToken.anchorHomepageUrl}
</a>
</div>
</section>
);

interface OfframpSummaryDialogProps {
fromToken: InputTokenDetails;
toToken: OutputTokenDetails;
formToAmount: string;
fromAmountString: string;
visible: boolean;
tokenOutAmount: UseTokenOutAmountResult;
anchorUrl?: string;
onSubmit: () => void;
onClose: () => void;
}

export const OfframpSummaryDialog: FC<OfframpSummaryDialogProps> = ({
fromToken,
toToken,
visible,
formToAmount,
fromAmountString,
tokenOutAmount,
anchorUrl,
onClose,
onSubmit,
}) => {
const [isSubmitted, setIsSubmitted] = useState(false);
const { selectedNetwork } = useNetwork();
const fromIcon = useGetAssetIcon(fromToken.networkAssetIcon);
const toIcon = useGetAssetIcon(toToken.fiat.assetIcon);

const { feesCost } = useOfframpFees(tokenOutAmount.data?.roundedDownQuotedAmountOut || Big(0), toToken);

if (!visible) return null;
if (!anchorUrl) return null;

const content = (
<div className="flex flex-col justify-center">
<AssetDisplay
iconAlt={fromToken.networkAssetIcon}
symbol={fromToken.assetSymbol}
amount={fromAmountString}
iconSrc={fromIcon}
/>
<ArrowDownIcon className="w-4 h-4 my-2" />
<AssetDisplay amount={formToAmount} symbol={toToken.fiat.symbol} iconSrc={toIcon} iconAlt={toToken.fiat.symbol} />
<FeeDetails
fiatSymbol={toToken.fiat.symbol}
fromToken={fromToken}
toToken={toToken}
tokenOutAmount={tokenOutAmount}
network={selectedNetwork}
feesCost={feesCost}
/>
</div>
);
const actions = (
<button
disabled={isSubmitted}
className="btn-vortex-primary btn rounded-xl"
style={{ flex: '1 1 calc(50% - 0.75rem/2)' }}
onClick={() => {
setIsSubmitted(true);
onSubmit();
window.open(anchorUrl, '_blank');
}}
>
{isSubmitted ? (
<>
<Spinner /> Continue on Partner&apos;s page
</>
) : (
<>
Continue with Partner <ArrowTopRightOnSquareIcon className="w-4 h-4" />
</>
)}
</button>
);

return <Dialog content={content} visible={visible} actions={actions} headerText="You're selling" onClose={onClose} />;
};
3 changes: 3 additions & 0 deletions src/constants/tokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface Fiat {
}

export interface OutputTokenDetails {
anchorHomepageUrl: string;
tomlFileUrl: string;
decimals: number;
fiat: Fiat;
Expand Down Expand Up @@ -256,6 +257,7 @@ export function getInputTokenDetails(network: Networks, inputTokenType: InputTok
export type OutputTokenType = 'eurc' | 'ars';
export const OUTPUT_TOKEN_CONFIG: Record<OutputTokenType, OutputTokenDetails> = {
eurc: {
anchorHomepageUrl: 'https://mykobo.co',
tomlFileUrl: 'https://circle.anchor.mykobo.co/.well-known/stellar.toml',
decimals: 12,
fiat: {
Expand All @@ -281,6 +283,7 @@ export const OUTPUT_TOKEN_CONFIG: Record<OutputTokenType, OutputTokenDetails> =
supportsClientDomain: true,
},
ars: {
anchorHomepageUrl: 'https://home.anclap.com',
tomlFileUrl: 'https://api.anclap.com/.well-known/stellar.toml',
decimals: 12,
fiat: {
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/useOfframpFees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Big from 'big.js';
import { calculateTotalReceive } from '../components/FeeCollapse';
import { roundDownToTwoDecimals } from '../helpers/parseNumbers';
import { OutputTokenDetails } from '../constants/tokenConfig';

export const useOfframpFees = (toAmount: Big, toToken: OutputTokenDetails) => {
const toAmountFixed = roundDownToTwoDecimals(toAmount);
const totalReceive = calculateTotalReceive(toAmount, toToken);
const totalReceiveFormatted = roundDownToTwoDecimals(Big(totalReceive));
const feesCost = roundDownToTwoDecimals(Big(toAmountFixed || 0).sub(totalReceive));

return {
toAmountFixed,
totalReceive,
totalReceiveFormatted,
feesCost,
};
};
68 changes: 47 additions & 21 deletions src/pages/swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { swapConfirm } from './helpers/swapConfirm';
import { TrustedBy } from '../../components/TrustedBy';
import { WhyVortex } from '../../components/WhyVortex';
import { usePolkadotWalletState } from '../../contexts/polkadotWallet';
import { OfframpSummaryDialog } from '../../components/OfframpSummaryDialog';

export const SwapPage = () => {
const formRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -70,6 +71,8 @@ export const SwapPage = () => {
const [apiInitializeFailed, setApiInitializeFailed] = useState(false);
const [_, setIsReady] = useState(false);
const [showCompareFees, setShowCompareFees] = useState(false);
const [isOfframpSummaryDialogVisible, setIsOfframpSummaryDialogVisible] = useState(false);
const [cachedAnchorUrl, setCachedAnchorUrl] = useState<string | undefined>(undefined);
const [cachedId, setCachedId] = useState<string | undefined>(undefined);
const { trackEvent } = useEventsContext();
const { selectedNetwork, setNetworkSelectorDisabled } = useNetwork();
Expand Down Expand Up @@ -131,6 +134,14 @@ export const SwapPage = () => {
}
}, [firstSep24ResponseState?.id]);

// Store the anchor URL when it becomes available
useEffect(() => {
if (firstSep24ResponseState?.url) {
setCachedAnchorUrl(firstSep24ResponseState.url);
setIsOfframpSummaryDialogVisible(true);
}
}, [firstSep24ResponseState?.url]);

const {
isTokenSelectModalVisible,
tokenSelectModalType,
Expand Down Expand Up @@ -199,6 +210,7 @@ export const SwapPage = () => {
// We don't automatically close the window, as this could be confusing for the user.
// event.source.close();

setIsOfframpSummaryDialogVisible(false);
showToast(ToastMessage.KYC_COMPLETED);
}
};
Expand Down Expand Up @@ -344,6 +356,11 @@ export const SwapPage = () => {
const onSwapConfirm = (e: Event) => {
e.preventDefault();

if (offrampStarted) {
setIsOfframpSummaryDialogVisible(true);
return;
}

if (!termsAccepted && !termsChecked) {
setTermsError(true);

Expand All @@ -368,10 +385,25 @@ export const SwapPage = () => {
handleOnSubmit,
setTermsAccepted,
});

setIsOfframpSummaryDialogVisible(true);
};

const main = (
<main ref={formRef}>
<OfframpSummaryDialog
fromToken={fromToken}
fromAmountString={fromAmountString}
toToken={toToken}
formToAmount={formToAmount}
tokenOutAmount={tokenOutAmount}
visible={isOfframpSummaryDialogVisible}
anchorUrl={firstSep24ResponseState?.url || cachedAnchorUrl}
onSubmit={() => {
handleOnAnchorWindowOpen();
}}
onClose={() => setIsOfframpSummaryDialogVisible(false)}
/>
<SigningBox step={offrampSigningPhase} />
<motion.form
initial={{ scale: 0.9, opacity: 0 }}
Expand Down Expand Up @@ -432,27 +464,21 @@ export const SwapPage = () => {
>
Compare fees
</button>

{firstSep24ResponseState?.url !== undefined ? (
// eslint-disable-next-line react/jsx-no-target-blank
<a
href={firstSep24ResponseState.url}
target="_blank"
rel="opener" //noopener forbids the use of postMessages.
className="btn-vortex-primary btn rounded-xl"
style={{ flex: '1 1 calc(50% - 0.75rem/2)' }}
onClick={handleOnAnchorWindowOpen}
// open in a tinier window
>
Continue with Partner
</a>
) : (
<SwapSubmitButton
text={offrampInitiating ? 'Confirming' : offrampStarted ? 'Processing Details' : 'Confirm'}
disabled={Boolean(getCurrentErrorMessage()) || !inputAmountIsStable || !!initializeFailedMessage} // !!initializeFailedMessage we disable when the initialize failed message is not null
pending={offrampInitiating || offrampStarted || offrampState !== undefined}
/>
)}
<SwapSubmitButton
text={
offrampInitiating
? 'Confirming'
: offrampStarted && isOfframpSummaryDialogVisible
? 'Processing'
: 'Confirm'
}
disabled={Boolean(getCurrentErrorMessage()) || !inputAmountIsStable || !!initializeFailedMessage}
pending={
offrampInitiating ||
(offrampStarted && Boolean(cachedAnchorUrl) && isOfframpSummaryDialogVisible) ||
offrampState !== undefined
}
/>
</div>
<hr className="mt-6 mb-3" />
<PoweredBy />
Expand Down
Loading