Skip to content

Commit

Permalink
feat: add BuyFiatModal to handle purchase issues
Browse files Browse the repository at this point in the history
  • Loading branch information
r41ph committed Oct 16, 2024
1 parent 61e3578 commit cc6f2a3
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Box } from '@mui/material';

import OutlinedButton from 'web-components/src/components/buttons/OutlinedButton';
import Card from 'web-components/src/components/cards/Card';
import SellOrderNotFoundIcon from 'web-components/src/components/icons/SellOrderNotFoundIcon';
import Modal from 'web-components/src/components/modal';
import { Title } from 'web-components/src/components/typography';

import { UseStateSetter } from 'types/react/use-state';

import { UserCanPurchaseCreditsType } from './BuyFiatModal.types';

interface BuyFiatModalProps {
title: string;
content: React.ReactNode;
button: { text: string; href: string | null };
userCanPurchaseCredits: UserCanPurchaseCreditsType;
onClose: UseStateSetter<UserCanPurchaseCreditsType>;
handleClick: () => void;
}

export const BuyFiatModal = ({
title,
content,
button,
userCanPurchaseCredits,
onClose,
handleClick,
}: BuyFiatModalProps) => {
return (
<Modal
open={userCanPurchaseCredits.openModal}
onClose={() => onClose({ ...userCanPurchaseCredits, openModal: false })}
className="w-[560px] !py-40 !px-30"
>
<Box className="flex flex-col items-center">
<SellOrderNotFoundIcon className="w-[100px] h-[100px]" />
<Title variant="h4" className="text-center mt-20 px-30">
{title}
</Title>
<Card className="text-left w-full py-40 px-30 my-40">{content}</Card>
<OutlinedButton onClick={handleClick}>{button.text}</OutlinedButton>
</Box>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UserCanPurchaseCreditsType = {
openModal: boolean;
amountAvailable: number;
};
176 changes: 152 additions & 24 deletions web-marketplace/src/pages/BuyCredits/BuyCredits.Form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { msg, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe, Stripe, StripeElements } from '@stripe/stripe-js';
import { useQuery } from '@tanstack/react-query';
Expand All @@ -20,20 +22,26 @@ import { getAllowedDenomQuery } from 'lib/queries/react-query/ecocredit/marketpl
import { getPaymentMethodsQuery } from 'lib/queries/react-query/registry-server/getPaymentMethodsQuery/getPaymentMethodsQuery';
import { useWallet } from 'lib/wallet/wallet';

import { useFetchSellOrders } from 'features/marketplace/BuySellOrderFlow/hooks/useFetchSellOrders';
import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types';
import { normalizeToUISellOrderInfo } from 'pages/Projects/hooks/useProjectsSellOrders.utils';
import {
CREDIT_VINTAGE_OPTIONS,
CREDITS_AMOUNT,
CURRENCY,
CURRENCY_AMOUNT,
SELL_ORDERS,
} from 'components/molecules/CreditsAmount/CreditsAmount.constants';
import { getCurrencyAmount } from 'components/molecules/CreditsAmount/CreditsAmount.utils';
import { findDisplayDenom } from 'components/molecules/DenomLabel/DenomLabel.utils';
import { AgreePurchaseForm } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm';
import { AgreePurchaseFormSchemaType } from 'components/organisms/AgreePurchaseForm/AgreePurchaseForm.schema';
import { AgreePurchaseFormFiat } from 'components/organisms/AgreePurchaseForm/AgreePurchaseFormFiat';
import { BuyFiatModal } from 'components/organisms/BuyFiatModal/BuyFiatModal';
import { ChooseCreditsForm } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm';
import { ChooseCreditsFormSchemaType } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema';
import { CardSellOrder } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.types';
import { getFilteredCryptoSellOrders } from 'components/organisms/ChooseCreditsForm/ChooseCreditsForm.utils';
import { useLoginData } from 'components/organisms/LoginButton/hooks/useLoginData';
import { LoginFlow } from 'components/organisms/LoginFlow/LoginFlow';
import { PaymentInfoForm } from 'components/organisms/PaymentInfoForm/PaymentInfoForm';
Expand All @@ -49,6 +57,7 @@ import {
CardDetails,
PaymentOptionsType,
} from './BuyCredits.types';
import { findMatchingSellOrders } from './BuyCredits.utils';
import { usePurchase } from './hooks/usePurchase';

type Props = {
Expand All @@ -67,6 +76,7 @@ type Props = {
projectHref: string;
cardDetails?: CardDetails;
};

export const BuyCreditsForm = ({
paymentOption,
setPaymentOption,
Expand Down Expand Up @@ -95,13 +105,20 @@ export const BuyCreditsForm = ({
onButtonClick,
} = useLoginData({});
const navigate = useNavigate();
const { _ } = useLingui();

const setErrorBannerTextAtom = useSetAtom(errorBannerTextAtom);
const setConnectWalletModal = useSetAtom(connectWalletModalAtom);
const setSwitchWalletModalAtom = useSetAtom(switchWalletModalAtom);
const [paymentOptionCryptoClicked, setPaymentOptionCryptoClicked] = useAtom(
paymentOptionCryptoClickedAtom,
);
const [userCanPurchaseCredits, setUserCanPurchaseCredits] = useState({
openModal: false,
amountAvailable: 0,
});

const { refetchSellOrders } = useFetchSellOrders();

const cardDisabled = cardSellOrders.length === 0;

Expand Down Expand Up @@ -165,46 +182,120 @@ export const BuyCreditsForm = ({
stripe?: Stripe | null,
elements?: StripeElements | null,
) => {
const { retirementReason, country, stateProvince, postalCode } = values;
const {
sellOrders: selectedSellOrders,
email,
name,
savePaymentMethod,
createAccount: createActiveAccount,
// subscribeNewsletter, TODO
// followProject,
} = data;
const sellOrders = await refetchSellOrders();
const requestedSellOrders = findMatchingSellOrders(
data,
sellOrders?.map(normalizeToUISellOrderInfo),
);
const currentAvailableCredits = requestedSellOrders.reduce(
(credits, sellOrder) => credits + Number(sellOrder.quantity),
0,
);
const sellCanProceed =
data.creditsAmount && data.creditsAmount < currentAvailableCredits;
const partialCreditsAvailable =
data.creditsAmount && data.creditsAmount > currentAvailableCredits;

if (selectedSellOrders)
purchase({
paymentOption,
selectedSellOrders,
retiring,
retirementReason,
country,
stateProvince,
postalCode,
if (sellCanProceed) {
const { retirementReason, country, stateProvince, postalCode } = values;
const {
sellOrders: selectedSellOrders,
email,
name,
savePaymentMethod,
createActiveAccount,
paymentMethodId,
stripe,
elements,
confirmationTokenId,
createAccount: createActiveAccount,
// subscribeNewsletter, TODO
// followProject,
} = data;

if (selectedSellOrders)
purchase({
paymentOption,
selectedSellOrders,
retiring,
retirementReason,
country,
stateProvince,
postalCode,
email,
name,
savePaymentMethod,
createActiveAccount,
paymentMethodId,
stripe,
elements,
confirmationTokenId,
});
} else if (partialCreditsAvailable) {
setUserCanPurchaseCredits({
openModal: true,
amountAvailable: currentAvailableCredits,
});
} else {
setUserCanPurchaseCredits({
openModal: true,
amountAvailable: 0,
});
}
},
[
confirmationTokenId,
data,
paymentMethodId,
paymentOption,
purchase,
refetchSellOrders,
retiring,
],
);

const fiatModalConfig =
userCanPurchaseCredits.amountAvailable > 0
? {
title: _(
msg`Sorry, another user has purchased some or all of the credits you selected!`,
),
content: (
<>
<p className="uppercase font-muli text-sm font-extrabold pb-10">
<Trans>amount now available in</Trans>
{` ${findDisplayDenom({
allowedDenoms: allowedDenomsData?.allowedDenoms,
bankDenom: data?.currency?.askDenom!,
baseDenom: data?.currency?.askBaseDenom!,
})}`}
</p>
<span>{userCanPurchaseCredits.amountAvailable}</span>
</>
),
button: { text: _(msg`Choose new credits`), href: null },
}
: {
title: _(
msg`Sorry, another user has purchased all of the available credits from this project`,
),
content: (
<p className="text-lg pb-10 text-center">
<Trans>
Because we use blockchain technology, if another user purchases
the credits before you check out, you’ll need to choose
different credits.
</Trans>
</p>
),
button: { text: _(msg`search for new credits`), href: '/projects' },
};

const filteredCryptoSellOrders = useMemo(
() =>
getFilteredCryptoSellOrders({
askDenom: data.currency?.askDenom,
cryptoSellOrders,
retiring,
}),
[cryptoSellOrders, data.currency?.askDenom, retiring],
);

return (
<div className="flex">
<div>
Expand Down Expand Up @@ -329,6 +420,43 @@ export const BuyCreditsForm = ({
wallets={walletsUiConfig}
modalState={modalState}
/>
<BuyFiatModal
title={fiatModalConfig.title}
content={fiatModalConfig.content}
button={fiatModalConfig.button}
userCanPurchaseCredits={userCanPurchaseCredits}
onClose={setUserCanPurchaseCredits}
handleClick={() => {
// If there is no credits available, we need to navigate to the projects page
if (fiatModalConfig.button.href) {
navigate(fiatModalConfig.button.href);
} else {
setUserCanPurchaseCredits({
...userCanPurchaseCredits,
openModal: false,
});
// After a purchase attempt where there's partial credits availablility,
// we need to update the form with the new credits and currency amounts.
handleSaveNext({
...data,
[CREDITS_AMOUNT]: userCanPurchaseCredits.amountAvailable,
[CURRENCY_AMOUNT]: getCurrencyAmount({
currentCreditsAmount: userCanPurchaseCredits.amountAvailable,
card: paymentOption === PAYMENT_OPTIONS.CARD,
orderedSellOrders:
paymentOption === PAYMENT_OPTIONS.CARD
? cardSellOrders.sort((a, b) => a.usdPrice - b.usdPrice)
: filteredCryptoSellOrders?.sort(
(a, b) => Number(a.askAmount) - Number(b.askAmount),
) || [],
creditTypePrecision: creditTypeData?.creditType?.precision,
}).currencyAmount,
});
window.scrollTo(0, 0);
handleActiveStep(0);
}
}}
/>
</div>
);
};
14 changes: 12 additions & 2 deletions web-marketplace/src/pages/BuyCredits/BuyCredits.utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { msg } from '@lingui/macro';

import { Project } from 'generated/sanity-graphql';
import { TranslatorType } from 'lib/i18n/i18n.types';

import { UISellOrderInfo } from 'pages/Projects/AllProjects/AllProjects.types';

import { PAYMENT_OPTIONS } from './BuyCredits.constants';
import { PaymentOptionsType } from './BuyCredits.types';
import { BuyCreditsSchemaTypes, PaymentOptionsType } from './BuyCredits.types';

type GetFormModelParams = {
_: TranslatorType;
Expand Down Expand Up @@ -42,3 +41,14 @@ export const getFormModel = ({
],
};
};

export const findMatchingSellOrders = (
data: BuyCreditsSchemaTypes,
sellOrders: UISellOrderInfo[] | undefined,
) => {
if (!sellOrders) return [];

const sellOrderIds = data?.sellOrders?.map(order => order.sellOrderId);

return sellOrders.filter(order => sellOrderIds?.includes(order.id));
};

0 comments on commit cc6f2a3

Please sign in to comment.