From 1432967aa49812667c43f36be220065d465b6d24 Mon Sep 17 00:00:00 2001 From: Juraj Piar Date: Fri, 3 Sep 2021 11:36:12 +0100 Subject: [PATCH] RESOLVE CONFLICTS AND SQUASH ME! When done with conflicts, run: git rebase -i HEAD~ to reword message to: fix(hotfix): v1.4.2 --- package-lock.json | 6 +- package.json | 6 +- .../notifier/buy/CheckoutPayment.tsx | 154 ++++++++ .../buy/NotifierOfferCheckoutPage.tsx | 185 ++++++++++ .../mypurchase/NotifierMyPurchasePage.tsx | 335 ++++++++++++++++++ 5 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/components/organisms/notifier/buy/CheckoutPayment.tsx create mode 100644 src/components/pages/notifier/buy/NotifierOfferCheckoutPage.tsx create mode 100644 src/components/pages/notifier/mypurchase/NotifierMyPurchasePage.tsx diff --git a/package-lock.json b/package-lock.json index 7a29224c3..6b819f92a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,10 @@ { "name": "rif-marketplace-ui", +<<<<<<< HEAD "version": "1.3.5", +======= + "version": "1.4.2", +>>>>>>> db46ca1 (fix(hotfix): v1.4.2) "lockfileVersion": 1, "requires": true, "dependencies": { @@ -17297,4 +17301,4 @@ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5b463b49f..c6d46a94c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "rif-marketplace-ui", +<<<<<<< HEAD "version": "1.3.5", +======= + "version": "1.4.2", +>>>>>>> db46ca1 (fix(hotfix): v1.4.2) "description": "RIF Marketplace provides a digital catalogue with a wide range of decentralised services.", "keywords": [ "RIF", @@ -104,4 +108,4 @@ "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.ts" } } -} +} \ No newline at end of file diff --git a/src/components/organisms/notifier/buy/CheckoutPayment.tsx b/src/components/organisms/notifier/buy/CheckoutPayment.tsx new file mode 100644 index 000000000..feab880f5 --- /dev/null +++ b/src/components/organisms/notifier/buy/CheckoutPayment.tsx @@ -0,0 +1,154 @@ +import RoundBtn from 'components/atoms/RoundBtn' +import React, { + FC, useState, useEffect, useContext, +} from 'react' +import Typography from '@material-ui/core/Typography' +import GridRow from 'components/atoms/GridRow' +import Grid, { GridProps } from '@material-ui/core/Grid' +import { TokenXR } from 'models/Market' +import ExpirationDate from 'components/molecules/ExpirationDate' +import PriceSummary from 'components/molecules/PriceSummary' +import { makeStyles, Theme } from '@material-ui/core/styles' +import Box from '@material-ui/core/Box' +import RoundedCard from 'components/atoms/RoundedCard' +import Big from 'big.js' +import { Web3Store } from '@rsksmart/rif-ui' +import { getBalance } from 'contracts/utils/accountBalance' +import NotEnoughFunds from 'components/atoms/NotEnoughFunds' +import Web3 from 'web3' +import useErrorReporter from 'hooks/useErrorReporter' +import TermsAndConditions from 'components/organisms/TermsAndConditions' +import { TERMS_CONDITIONS_BUY } from 'constants/notifier/strings' + +type Props = { + onBuy: () => void + fiatDisplayName: string + expirationDate: Date + tokenXR: TokenXR + cryptoPrice: Big +} + +const useStyles = makeStyles((theme: Theme) => ({ + priceSummaryCard: { + padding: theme.spacing(1.5, 3), + marginBottom: theme.spacing(1), + }, + expirationDate: { + justifyContent: 'center', + }, +})) + +const CheckoutPayment: FC = ({ + onBuy, fiatDisplayName, expirationDate, tokenXR, cryptoPrice, +}) => { + const classes = useStyles() + const reportError = useErrorReporter() + + const { state: { web3, account } } = useContext(Web3Store) + const [hasEnoughFunds, setHasEnoughFunds] = useState(false) + const [isLoadingBalance, setIsLoadingBalance] = useState(false) + const [termsChecked, setTermsChecked] = useState(false) + + const { + symbol: selectedTokenSymbol, + } = tokenXR + + useEffect(() => { + const calculateBalance = async (): Promise => { + try { + setIsLoadingBalance(true) + const balance = await getBalance( + web3 as Web3, account as string, selectedTokenSymbol, + ) + setHasEnoughFunds(Big(balance).gte(cryptoPrice)) + } catch (error) { + reportError({ + error, + id: 'get-balance', + text: 'Could not read account balance', + }) + } finally { + setIsLoadingBalance(false) + } + } + calculateBalance() + }, [account, web3, selectedTokenSymbol, cryptoPrice, reportError]) + + const handleTermsChange = (): void => setTermsChecked((prev) => !prev) + + const colProps: GridProps = { + container: true, + item: true, + direction: 'column', + md: 6, + sm: 12, + alignItems: 'center', + justify: 'center', + } + const isBuyDisabled = !hasEnoughFunds || isLoadingBalance || !termsChecked + + return ( + <> + + + {`To acquire this notification service you have to select + the currency to get the final price.`} + + + You can add more events to this contract before or after renew. + + + + + + + + + Expiration date + + + + + { + !isLoadingBalance && !hasEnoughFunds && ( + + ) + } + + + + + + Buy + + + + {`Your wallet will open and you will be asked + to confirm the transaction for buying the notification plan.`} + + + + + ) +} + +export default CheckoutPayment diff --git a/src/components/pages/notifier/buy/NotifierOfferCheckoutPage.tsx b/src/components/pages/notifier/buy/NotifierOfferCheckoutPage.tsx new file mode 100644 index 000000000..9e7666cd9 --- /dev/null +++ b/src/components/pages/notifier/buy/NotifierOfferCheckoutPage.tsx @@ -0,0 +1,185 @@ +import React, { + FC, useContext, useState, +} from 'react' +import Grid from '@material-ui/core/Grid' +import Typography from '@material-ui/core/Typography' +import { + NotifierOffersContextProps as ContextProps, + NotifierOffersContext, +} from 'context/Services/notifier/offers' +import CenteredPageTemplate from 'components/templates/CenteredPageTemplate' +import NotifierPlanDescription from 'components/organisms/notifier/NotifierPlanDescription' +import MarketContext, { MarketContextProps } from 'context/Market' +import CheckoutStepper from 'components/organisms/notifier/buy/CheckoutStepper' +import { NotifierEventItem } from 'models/marketItems/NotifierEventItem' +import NotifierContract from 'contracts/notifier/Notifier' +import Web3 from 'web3' +import { Web3Store } from '@rsksmart/rif-ui' +import useErrorReporter from 'hooks/useErrorReporter' +import ProgressOverlay from 'components/templates/ProgressOverlay' +import RoundBtn from 'components/atoms/RoundBtn' +import ROUTES from 'routes' +import { useHistory } from 'react-router-dom' +import { ConfirmationsContext } from 'context/Confirmations' +import { convertToWeiString } from 'utils/parsers' +import WithLoginCard from 'components/hoc/WithLoginCard' +import { getOrCreateSubscription } from 'api/rif-notifier-service/subscriptionUtils' + +const NotifierOfferCheckoutPage: FC = () => { + const { + state: { account, web3 }, + } = useContext(Web3Store) + const { + state: { + exchangeRates: { + currentFiat: { + displayName: currentFiat, + }, + crypto, + }, + }, + } = useContext(MarketContext) + const { dispatch: confirmationsDispatch } = useContext(ConfirmationsContext) + + const { + state: { + order, + }, + } = useContext(NotifierOffersContext) + const reportError = useErrorReporter() + const history = useHistory() + + const [isProcessingTx, setIsProcessingTx] = useState(false) + const [txOperationDone, setTxOperationDone] = useState(false) + const [eventsAdded, setEventsAdded] = useState([]) + + if (!order?.item) { + history.push(ROUTES.NOTIFIER.BUY.BASE) + return null + } + + const handleEventRemoved = ( + { id: notifierEventId }: NotifierEventItem, + ): void => { + const filteredEvents = eventsAdded.filter( + ({ id }) => id !== notifierEventId, + ) + setEventsAdded(filteredEvents) + } + + const handleEventItemAdded = ( + eventItem: NotifierEventItem, + ): void => { + setEventsAdded([...eventsAdded, eventItem]) + } + + const handleOnBuy = async (): Promise => { + if (!account) return // wrapped with withLoginCard + try { + setIsProcessingTx(true) + + const { item } = order + const { + provider: providerAddress, + value: amount, + token, + planId, + url, + } = item + + const { symbol, tokenAddress } = token + + const { + hash: subscriptionHash, signature, + } = await getOrCreateSubscription({ + planId, symbol, url, value: amount, + }, eventsAdded, account, reportError) + + if (!subscriptionHash) return + + const purchaseReceipt = await NotifierContract.getInstance(web3 as Web3) + .createSubscription( + { + subscriptionHash, + providerAddress, + signature, + amount, + tokenAddress, + }, + { + from: account, + value: convertToWeiString(amount), + }, + ) + + if (purchaseReceipt) { + setTxOperationDone(true) + confirmationsDispatch({ + type: 'NEW_REQUEST', + payload: { + contractAction: 'NOTIFIER_CREATE_SUBSCRIPTION', + txHash: purchaseReceipt.transactionHash, + }, + }) + } + } catch (error) { + const { customMessage } = error + reportError({ + error, + id: 'contract-notifier', + text: customMessage || 'Could not complete the order', + }) + } finally { + setIsProcessingTx(false) + } + } + + return ( + + + + Notification plan selected + + + + + history.push(ROUTES.NOTIFIER.MYPURCHASES.BASE) + } + > + View my purchases + , + history.push(ROUTES.NOTIFIER.BUY.BASE) + } + > + View offers listing + , + ]} + + /> + + ) +} + +export default WithLoginCard({ + WrappedComponent: NotifierOfferCheckoutPage, + title: 'Please, connect your wallet.', + contentText: 'Connect your wallet in order to proceed to the checkout.', +}) diff --git a/src/components/pages/notifier/mypurchase/NotifierMyPurchasePage.tsx b/src/components/pages/notifier/mypurchase/NotifierMyPurchasePage.tsx new file mode 100644 index 000000000..2f35ea2c3 --- /dev/null +++ b/src/components/pages/notifier/mypurchase/NotifierMyPurchasePage.tsx @@ -0,0 +1,335 @@ +import { makeStyles, Theme } from '@material-ui/core/styles' +import Typography from '@material-ui/core/Typography' +import { + shortenString, Web3Store, Spinner, +} from '@rsksmart/rif-ui' +import { notifierSubscriptionsAddress } from 'api/rif-marketplace-cache/notifier/subscriptions' +import GridRow from 'components/atoms/GridRow' +import RoundedCard from 'components/atoms/RoundedCard' +import WithLoginCard from 'components/hoc/WithLoginCard' +import InfoBar from 'components/molecules/InfoBar' +import MyPurchasesHeader from 'components/molecules/MyPurchasesHeader' +import PurchasesTable, { MySubscription } from 'components/organisms/notifier/mypurchase/PurchasesTable' +import CenteredPageTemplate from 'components/templates/CenteredPageTemplate' +import AppContext, { AppContextProps } from 'context/App' +import MarketContext from 'context/Market' +import useConfirmations from 'hooks/useConfirmations' +import useErrorReporter from 'hooks/useErrorReporter' +import { NotifierSubscriptionItem } from 'models/marketItems/NotifierItem' +import { UIError } from 'models/UIMessage' +import React, { + FC, useContext, useEffect, useState, + useCallback, +} from 'react' +import { getShortDateString } from 'utils/dateUtils' +import { shortChecksumAddress } from 'utils/stringUtils' +import { getFiatPrice } from 'utils/priceUtils' +import { SubscriptionDetails, subscriptionHeaders } from 'components/organisms/notifier/mypurchase/details' +import NotifierDetails, { SubscriptionEventsDisplayItem } from 'components/organisms/notifier/details/NotifierDetailsModal' +import RoundBtn from 'components/atoms/RoundBtn' +import { eventDisplayItemIterator } from 'components/organisms/notifier/details/utils' +import { SUBSCRIPTION_STATUSES } from 'api/rif-notifier-service/models/subscriptions' +import ProgressOverlay from 'components/templates/ProgressOverlay' +import ROUTES from 'routes' +import NotifierContract from 'contracts/notifier/Notifier' +import Web3 from 'web3' +import { convertToWeiString } from 'utils/parsers' +import { useHistory } from 'react-router-dom' +import { ConfirmationsContext } from 'context/Confirmations' +import { getOrCreateRenewalSubscription } from 'api/rif-notifier-service/subscriptionUtils' +import GridItem from 'components/atoms/GridItem' +import FeatureNotSupportedButton from 'components/atoms/FeatureNotSupportedButton' +import Refresh from 'components/molecules/Refresh' +import mapMyPurchases from './mapMyPurchases' +import { setBrowserSessionCannotRenew } from './utils' + +const useStyles = makeStyles((theme: Theme) => ({ + titleContainer: { + padding: theme.spacing(2, 2, 0, 2), + }, + refreshContainer: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + }, +})) + +const NotifierMyPurchasePage: FC = () => { + const classes = useStyles() + + const { + state: { account, web3 }, + } = useContext(Web3Store) + const { + state: { + apis: { + [notifierSubscriptionsAddress]: subscriptionsApi, + }, + }, + } = useContext(AppContext) + const { + state: { + exchangeRates, + }, + } = useContext(MarketContext) + const { dispatch: confirmationsDispatch } = useContext(ConfirmationsContext) + + const history = useHistory() + const reportError = useErrorReporter() + + const [ + subscriptions, + setSubscriptions, + ] = useState>() + const [ + subscriptionDetails, + setSubscriptionDetails, + ] = useState() + const [ + subscriptionEvents, + setSubscriptionEvents, + ] = useState>() + + const [isProcessingTx, setIsProcessingTx] = useState(false) + const [txOperationDone, setTxOperationDone] = useState(false) + const [isLoadingData, setIsLoadingData] = useState(true) + + const numberOfConfs = useConfirmations( + ['NOTIFIER_CREATE_SUBSCRIPTION'], + ).length + const isAwaitingConfs = Boolean(numberOfConfs) + + const fetchSubscriptions = useCallback(() => { + if (account && subscriptionsApi) { + setIsLoadingData(true) + subscriptionsApi.connect(reportError) + subscriptionsApi.fetch({ + consumer: account, + status: { + $ne: SUBSCRIPTION_STATUSES.PENDING, + }, + }) + .then((incomingSubscriptions: Array) => { + const prevSubsMap = incomingSubscriptions.reduce((acc, + sub) => ({ ...acc, [sub.previousSubscription]: sub }), + {}) + + const filtered = incomingSubscriptions.filter( + ({ id }) => !prevSubsMap[id], + ) + + setSubscriptions(Object.values(filtered)) + }) + .catch((error) => reportError(new UIError({ + id: 'service-fetch', + text: 'Error while fetching subscriptions.', + error, + }))) + .finally(() => { + setIsLoadingData(false) + }) + } + }, [subscriptionsApi, account, reportError]) + + useEffect(() => { + fetchSubscriptions() + }, [fetchSubscriptions]) + + const { + crypto, + currentFiat: { + displayName: fiatDisplayName, + }, + } = exchangeRates + + const onView = (subscriptionId: string): void => { + const subscription: NotifierSubscriptionItem = subscriptions + ?.find(({ id }) => id === subscriptionId) as NotifierSubscriptionItem + + if (!subscription) return + + const { + id, + notificationBalance, + plan: { channels }, + expirationDate, + price, + token: { symbol: tokenSymbol }, + events, + provider, + } = subscription + const { provider: providerAddress } = provider + + const viewItem: typeof subscriptionDetails = { + id: shortenString(id), + provider: shortChecksumAddress(providerAddress), + amount: String(notificationBalance), + channels: channels?.map(({ type }) => type).join(',') || '', + expDate: getShortDateString(expirationDate), + price: `${getFiatPrice(price, crypto[tokenSymbol])} ${fiatDisplayName}`, + } + + setSubscriptionDetails(viewItem) + setSubscriptionEvents(events.map( + (event) => eventDisplayItemIterator(event, channels), + )) + } + + const onRenew = async (subscriptionId: string): Promise => { + const subscription: NotifierSubscriptionItem = subscriptions + ?.find(({ id }) => id === subscriptionId) as NotifierSubscriptionItem + + if (!subscription || !account) return + + try { + setIsProcessingTx(true) + const { + id: subscriptionHash, + plan: { planId }, + price, + token: { symbol: tokenSymbol, tokenAddress }, + provider: { provider: providerAddress, url: providerUrl }, + } = subscription + + const response = await getOrCreateRenewalSubscription( + subscriptionHash, { + value: price, symbol: tokenSymbol, planId, url: providerUrl, + }, account, reportError, + ) + + const { hash: renewalHash, signature } = response + + const purchaseReceipt = await NotifierContract.getInstance(web3 as Web3) + .createSubscription( + { + subscriptionHash: renewalHash, + providerAddress, + signature, + amount: price, + tokenAddress, + }, + { + from: account, + value: convertToWeiString(price), + }, + ) + + if (purchaseReceipt) { + setTxOperationDone(true) + setBrowserSessionCannotRenew(subscriptionHash) + confirmationsDispatch({ + type: 'NEW_REQUEST', + payload: { + contractAction: 'NOTIFIER_CREATE_SUBSCRIPTION', + txHash: purchaseReceipt.transactionHash, + }, + }) + } + } catch (error) { + const { customMessage } = error + reportError({ + error, + id: 'contract-notifier', + text: customMessage || 'Could not complete the order', + }) + } finally { + setIsProcessingTx(false) + } + } + + const items = subscriptions?.map(mapMyPurchases( + exchangeRates, { onView, onRenew }, + )) || [] as Array + + const onModalClose = (): void => { + setSubscriptionDetails(undefined) + setSubscriptionEvents([]) + } + + return ( + + + <> + + + + + + Active plans + + + + + + + + { + isLoadingData + ? + : + } + + + {subscriptionDetails + && ( + + Cancel Plan + + )} + /> + )} + + setTxOperationDone(false) + } + > + View my purchases + , + history.push(ROUTES.NOTIFIER.BUY.BASE) + } + > + View offers listing + , + ]} + /> + + ) +} + +export default WithLoginCard({ + WrappedComponent: NotifierMyPurchasePage, + title: 'Connect your wallet to see your purchases', + contentText: 'Connect your wallet to get detailed information about your purchases', +})