Commit d400b909 authored by Greg Bugyis's avatar Greg Bugyis Committed by GitHub

feat: Buy Bag events (#5062)

* Add activity boolean to Collection page event

* Log add-to-bag events

* WIP

* Bag update events

* Log pay event

* Bag success event

* Add bag signed event

* Move formatting function out to util

* Format event properties with utility function

* Remove console log and fix event on details page

* Remove commented code

* Fix event names to follow convention

* Move priceChangedAssets logging to useEffect

* Add modal constant and useTrace

* Fix typo
parent fda9d29d
...@@ -16,6 +16,12 @@ export enum EventName { ...@@ -16,6 +16,12 @@ export enum EventName {
NAVBAR_SEARCH_SELECTED = 'Navbar Search Selected', NAVBAR_SEARCH_SELECTED = 'Navbar Search Selected',
NAVBAR_SEARCH_EXITED = 'Navbar Search Exited', NAVBAR_SEARCH_EXITED = 'Navbar Search Exited',
NFT_ACTIVITY_SELECTED = 'NFT Activity Selected', NFT_ACTIVITY_SELECTED = 'NFT Activity Selected',
NFT_BUY_ADDED = 'NFT Buy Bag Added',
NFT_BUY_BAG_CHANGED = 'NFT Buy Bag Changed',
NFT_BUY_BAG_PAY = 'NFT Buy Bag Pay Clicked',
NFT_BUY_BAG_REFUNDED = 'NFT Buy Bag Refunded',
NFT_BUY_BAG_SIGNED = 'NFT Buy Bag Signed',
NFT_BUY_BAG_SUCCEEDED = 'NFT Buy Bag Succeeded',
NFT_FILTER_OPENED = 'NFT Collection Filter Opened', NFT_FILTER_OPENED = 'NFT Collection Filter Opened',
NFT_FILTER_SELECTED = 'NFT Filter Selected', NFT_FILTER_SELECTED = 'NFT Filter Selected',
NFT_TRENDING_ROW_SELECTED = 'Trending Row Selected', NFT_TRENDING_ROW_SELECTED = 'Trending Row Selected',
...@@ -106,6 +112,7 @@ export enum SectionName { ...@@ -106,6 +112,7 @@ export enum SectionName {
/** Known modals for analytics purposes. */ /** Known modals for analytics purposes. */
export enum ModalName { export enum ModalName {
CONFIRM_SWAP = 'confirm-swap-modal', CONFIRM_SWAP = 'confirm-swap-modal',
NFT_TX_COMPLETE = 'nft-tx-complete-modal',
TOKEN_SELECTOR = 'token-selector-modal', TOKEN_SELECTOR = 'token-selector-modal',
// alphabetize additional modal names. // alphabetize additional modal names.
} }
...@@ -125,6 +132,7 @@ export enum ElementName { ...@@ -125,6 +132,7 @@ export enum ElementName {
MAX_TOKEN_AMOUNT_BUTTON = 'max-token-amount-button', MAX_TOKEN_AMOUNT_BUTTON = 'max-token-amount-button',
NAVBAR_SEARCH_INPUT = 'navbar-search-input', NAVBAR_SEARCH_INPUT = 'navbar-search-input',
NFT_ACTIVITY_TAB = 'nft-activity-tab', NFT_ACTIVITY_TAB = 'nft-activity-tab',
NFT_BUY_BAG_PAY_BUTTON = 'nft-buy-bag-pay-button',
NFT_FILTER_BUTTON = 'nft-filter-button', NFT_FILTER_BUTTON = 'nft-filter-button',
NFT_FILTER_OPTION = 'nft-filter-option', NFT_FILTER_OPTION = 'nft-filter-option',
NFT_TRENDING_ROW = 'nft-trending-row', NFT_TRENDING_ROW = 'nft-trending-row',
......
...@@ -20,11 +20,14 @@ import { ...@@ -20,11 +20,14 @@ import {
} from 'nft/hooks' } from 'nft/hooks'
import { fetchRoute } from 'nft/queries' import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types' import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import { buildSellObject } from 'nft/utils/buildSellObject' import {
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice' buildSellObject,
import { fetchPrice } from 'nft/utils/fetchPrice' fetchPrice,
formatAssetEventProperties,
recalculateBagUsingPooledAssets,
sortUpdatedAssets,
} from 'nft/utils'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute' import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { sortUpdatedAssets } from 'nft/utils/updatedAssets'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query' import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
...@@ -283,6 +286,10 @@ const Bag = () => { ...@@ -283,6 +286,10 @@ const Bag = () => {
bagStatus={bagStatus} bagStatus={bagStatus}
fetchAssets={fetchAssets} fetchAssets={fetchAssets}
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)} assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
eventProperties={{
usd_value: totalUsdPrice,
...formatAssetEventProperties(itemsInBag.map((item) => item.asset)),
}}
/> />
)} )}
{isSellingAssets && isProfilePage && ( {isSellingAssets && isProfilePage && (
......
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow' import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow'
import { Column } from 'nft/components/Flex' import { Column } from 'nft/components/Flex'
import { useBag, useIsMobile } from 'nft/hooks' import { useBag, useIsMobile } from 'nft/hooks'
import { BagItemStatus, BagStatus } from 'nft/types' import { BagItemStatus, BagStatus } from 'nft/types'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice' import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice' import { fetchPrice } from 'nft/utils/fetchPrice'
import { formatAssetEventProperties } from 'nft/utils/formatEventProperties'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
...@@ -44,24 +48,41 @@ export const BagContent = () => { ...@@ -44,24 +48,41 @@ export const BagContent = () => {
const hasAssetsInReview = priceChangedAssets.length > 0 const hasAssetsInReview = priceChangedAssets.length > 0
const hasAssets = itemsInBag.length > 0 const hasAssets = itemsInBag.length > 0
if (hasAssetsInReview)
sendAnalyticsEvent(EventName.NFT_BUY_BAG_CHANGED, {
usd_value: fetchedPriceData,
bag_quantity: itemsInBag,
...formatAssetEventProperties(priceChangedAssets),
})
if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) { if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) {
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW) if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG) else setBagStatus(BagStatus.ADDING_TO_BAG)
} }
}, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus]) }, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus, fetchedPriceData])
return ( return (
<> <>
<Column display={priceChangedAssets.length > 0 || unavailableAssets.length > 0 ? 'flex' : 'none'}> <Column display={priceChangedAssets.length > 0 || unavailableAssets.length > 0 ? 'flex' : 'none'}>
{unavailableAssets.length > 0 && ( {unavailableAssets.length > 0 && (
<UnavailableAssetsHeaderRow <Trace
assets={unavailableAssets} name={EventName.NFT_BUY_BAG_CHANGED}
usdPrice={fetchedPriceData} properties={{
clearUnavailableAssets={() => setItemsInBag(availableItems)} usd_value: fetchedPriceData,
didOpenUnavailableAssets={didOpenUnavailableAssets} bag_quantity: itemsInBag,
setDidOpenUnavailableAssets={setDidOpenUnavailableAssets} ...formatAssetEventProperties(unavailableAssets),
isMobile={isMobile} }}
/> shouldLogImpression
>
<UnavailableAssetsHeaderRow
assets={unavailableAssets}
usdPrice={fetchedPriceData}
clearUnavailableAssets={() => setItemsInBag(availableItems)}
didOpenUnavailableAssets={didOpenUnavailableAssets}
setDidOpenUnavailableAssets={setDidOpenUnavailableAssets}
isMobile={isMobile}
/>
</Trace>
)} )}
{priceChangedAssets.map((asset, index) => ( {priceChangedAssets.map((asset, index) => (
<PriceChangeBagRow <PriceChangeBagRow
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import Loader from 'components/Loader' import Loader from 'components/Loader'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
...@@ -48,6 +50,7 @@ interface BagFooterProps { ...@@ -48,6 +50,7 @@ interface BagFooterProps {
bagStatus: BagStatus bagStatus: BagStatus
fetchAssets: () => void fetchAssets: () => void
assetsAreInReview: boolean assetsAreInReview: boolean
eventProperties: Record<string, unknown>
} }
const PENDING_BAG_STATUSES = [ const PENDING_BAG_STATUSES = [
...@@ -65,6 +68,7 @@ export const BagFooter = ({ ...@@ -65,6 +68,7 @@ export const BagFooter = ({
bagStatus, bagStatus,
fetchAssets, fetchAssets,
assetsAreInReview, assetsAreInReview,
eventProperties,
}: BagFooterProps) => { }: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal() const toggleWalletModal = useToggleWalletModal()
const walletModalIsOpen = useModalIsOpen(ApplicationModal.WALLET) const walletModalIsOpen = useModalIsOpen(ApplicationModal.WALLET)
...@@ -102,28 +106,36 @@ export const BagFooter = ({ ...@@ -102,28 +106,36 @@ export const BagFooter = ({
)} )}
</WarningText> </WarningText>
)} )}
<Row <TraceEvent
as="button" events={[Event.onClick]}
color="explicitWhite" name={EventName.NFT_BUY_BAG_PAY}
className={styles.payButton} element={ElementName.NFT_BUY_BAG_PAY_BUTTON}
disabled={isDisabled} properties={{ ...eventProperties }}
onClick={() => { shouldLogImpression={isConnected && !isDisabled}
if (!isConnected) {
toggleWalletModal()
} else {
fetchAssets()
}
}}
> >
{isPending && <Loader size="20px" stroke="white" />} <Row
{!isConnected || walletModalIsOpen as="button"
? 'Connect wallet' color="explicitWhite"
: bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET className={styles.payButton}
? 'Proceed in wallet' disabled={isDisabled}
: bagStatus === BagStatus.PROCESSING_TRANSACTION onClick={() => {
? 'Transaction pending' if (!isConnected) {
: 'Pay'} toggleWalletModal()
</Row> } else {
fetchAssets()
}
}}
>
{isPending && <Loader size="20px" stroke="white" />}
{!isConnected || walletModalIsOpen
? 'Connect wallet'
: bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET
? 'Proceed in wallet'
: bagStatus === BagStatus.PROCESSING_TRANSACTION
? 'Transaction pending'
: 'Pay'}
</Row>
</TraceEvent>
</Footer> </Footer>
</Column> </Column>
) )
......
import { ChainId } from '@uniswap/smart-order-router' import { ChainId } from '@uniswap/smart-order-router'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, PageName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
...@@ -96,6 +99,15 @@ export const BuyCell = ({ ...@@ -96,6 +99,15 @@ export const BuyCell = ({
return itemsInBag.some((item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address) return itemsInBag.some((item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address)
}, [asset, itemsInBag]) }, [asset, itemsInBag])
const trace = useTrace({ page: PageName.NFT_COLLECTION_PAGE })
const eventProperties = {
collection_address: asset.address,
token_id: asset.tokenId,
token_type: asset.tokenType,
...trace,
}
return ( return (
<Column display={{ sm: 'none', lg: 'flex' }} height="full" justifyContent="center" marginX="auto"> <Column display={{ sm: 'none', lg: 'flex' }} height="full" justifyContent="center" marginX="auto">
{event.eventType === ActivityEventType.Listing && event.orderStatus ? ( {event.eventType === ActivityEventType.Listing && event.orderStatus ? (
...@@ -106,6 +118,7 @@ export const BuyCell = ({ ...@@ -106,6 +118,7 @@ export const BuyCell = ({
e.preventDefault() e.preventDefault()
isSelected ? removeAsset([asset]) : selectAsset([asset]) isSelected ? removeAsset([asset]) : selectAsset([asset])
!isSelected && !cartExpanded && !isMobile && toggleCart() !isSelected && !cartExpanded && !isMobile && toggleCart()
!isSelected && sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { eventProperties })
}} }}
disabled={event.orderStatus !== OrderStatus.VALID} disabled={event.orderStatus !== OrderStatus.VALID}
> >
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, PageName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { GenieAsset, Markets, UniformHeight } from 'nft/types' import { GenieAsset, Markets, UniformHeight } from 'nft/types'
import { formatWeiToDecimal, isAudio, isVideo, rarityProviderLogo } from 'nft/utils' import { formatWeiToDecimal, isAudio, isVideo, rarityProviderLogo } from 'nft/utils'
...@@ -36,6 +39,7 @@ export const CollectionAsset = ({ ...@@ -36,6 +39,7 @@ export const CollectionAsset = ({
const itemsInBag = useBag((state) => state.itemsInBag) const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
const toggleBag = useBag((state) => state.toggleBag) const toggleBag = useBag((state) => state.toggleBag)
const trace = useTrace({ page: PageName.NFT_COLLECTION_PAGE })
const { quantity, isSelected } = useMemo(() => { const { quantity, isSelected } = useMemo(() => {
return { return {
...@@ -72,6 +76,13 @@ export const CollectionAsset = ({ ...@@ -72,6 +76,13 @@ export const CollectionAsset = ({
} }
}, [asset]) }, [asset])
const eventProperties = {
collection_address: asset.address,
token_id: asset.tokenId,
token_type: asset.tokenType,
...trace,
}
return ( return (
<Card.Container <Card.Container
asset={asset} asset={asset}
...@@ -79,6 +90,7 @@ export const CollectionAsset = ({ ...@@ -79,6 +90,7 @@ export const CollectionAsset = ({
addAssetToBag={() => { addAssetToBag={() => {
addAssetsToBag([asset]) addAssetsToBag([asset])
!bagExpanded && !isMobile && toggleBag() !bagExpanded && !isMobile && toggleBag()
sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { ...eventProperties })
}} }}
removeAssetFromBag={() => { removeAssetFromBag={() => {
removeAssetsFromBag([asset]) removeAssetsFromBag([asset])
......
import { EventName, ModalName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { useTrace } from 'analytics/Trace'
import clsx from 'clsx' import clsx from 'clsx'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal' import { Portal } from 'nft/components/common/Portal'
...@@ -16,6 +19,7 @@ import { ...@@ -16,6 +19,7 @@ import {
parseTransactionResponse, parseTransactionResponse,
shortenTxHash, shortenTxHash,
} from 'nft/utils' } from 'nft/utils'
import { formatAssetEventProperties } from 'nft/utils/formatEventProperties'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
...@@ -33,6 +37,7 @@ const TxCompleteModal = () => { ...@@ -33,6 +37,7 @@ const TxCompleteModal = () => {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const txHashUrl = getExplorerLink(1, txHash, ExplorerDataType.TRANSACTION) const txHashUrl = getExplorerLink(1, txHash, ExplorerDataType.TRANSACTION)
const shouldShowModal = (txState === TxStateType.Success || txState === TxStateType.Failed) && txState const shouldShowModal = (txState === TxStateType.Success || txState === TxStateType.Failed) && txState
const trace = useTrace({ modal: ModalName.NFT_TX_COMPLETE })
const { const {
nftsPurchased, nftsPurchased,
nftsNotPurchased, nftsNotPurchased,
...@@ -73,229 +78,264 @@ const TxCompleteModal = () => { ...@@ -73,229 +78,264 @@ const TxCompleteModal = () => {
<Box className={styles.modalContainer} onClick={closeTxCompleteScreen}> <Box className={styles.modalContainer} onClick={closeTxCompleteScreen}>
{/* Successfully purchased NFTs */} {/* Successfully purchased NFTs */}
{showPurchasedModal && ( {showPurchasedModal && (
<Box className={styles.successModal} onClick={stopPropagation}> <Trace
<UniIcon color={vars.color.pink400} width="36" height="36" className={styles.uniLogo} /> name={EventName.NFT_BUY_BAG_SUCCEEDED}
<Box display="flex" flexWrap="wrap" width="full" height="min"> properties={{
<h1 className={styles.title}>Complete!</h1> buy_quantity: nftsPurchased.length,
<p className={styles.subHeading}>Uniswap has granted your wish!</p> usd_value: totalPurchaseValue,
</Box> transaction_hash: txHash,
<Box ...formatAssetEventProperties(nftsPurchased),
className={styles.successAssetsContainer} ...trace,
style={{ }}
maxHeight: nftsPurchased.length > 32 ? (isMobile ? '172px' : '292px') : 'min-content', shouldLogImpression
}} >
> <Box className={styles.successModal} onClick={stopPropagation}>
{[...nftsPurchased].map((nft, index) => ( <UniIcon color={vars.color.pink400} width="36" height="36" className={styles.uniLogo} />
<img <Box display="flex" flexWrap="wrap" width="full" height="min">
className={clsx( <h1 className={styles.title}>Complete!</h1>
styles.successAssetImage, <p className={styles.subHeading}>Uniswap has granted your wish!</p>
nftsPurchased.length > 1 && styles.successAssetImageGrid </Box>
)} <Box
style={{ className={styles.successAssetsContainer}
maxHeight: `${getSuccessfulImageSize(nftsPurchased.length, isMobile)}px`, style={{
maxWidth: `${getSuccessfulImageSize(nftsPurchased.length, isMobile)}px`, maxHeight: nftsPurchased.length > 32 ? (isMobile ? '172px' : '292px') : 'min-content',
}} }}
src={nft.imageUrl} >
alt={nft.name} {[...nftsPurchased].map((nft, index) => (
key={index} <img
/> className={clsx(
))} styles.successAssetImage,
</Box> nftsPurchased.length > 1 && styles.successAssetImageGrid
{nftsPurchased.length > 32 && <Box className={styles.overflowFade} />} )}
<Box style={{
display="flex" maxHeight: `${getSuccessfulImageSize(nftsPurchased.length, isMobile)}px`,
width="full" maxWidth: `${getSuccessfulImageSize(nftsPurchased.length, isMobile)}px`,
height="min" }}
flexDirection="row" src={nft.imageUrl}
marginTop={{ sm: '20', md: '20' }} alt={nft.name}
flexWrap={{ sm: 'wrap', md: 'nowrap' }} key={index}
alignItems="center" />
paddingRight={'40'} ))}
paddingLeft={'40'} </Box>
className={styles.bottomBar} {nftsPurchased.length > 32 && <Box className={styles.overflowFade} />}
justifyContent="space-between" <Box
> display="flex"
<Row> width="full"
<Box marginRight="16"> height="min"
{nftsPurchased.length} NFT{nftsPurchased.length === 1 ? '' : 's'} flexDirection="row"
</Box> marginTop={{ sm: '20', md: '20' }}
<Box>{formatEthPrice(totalPurchaseValue.toString())} ETH</Box> flexWrap={{ sm: 'wrap', md: 'nowrap' }}
</Row> alignItems="center"
<a href={txHashUrl} target="_blank" rel="noreferrer"> paddingRight={'40'}
<Box color="textPrimary" fontWeight="normal"> paddingLeft={'40'}
{shortenTxHash(txHash, 2, 2)} className={styles.bottomBar}
</Box> justifyContent="space-between"
</a> >
<Row>
<Box marginRight="16">
{nftsPurchased.length} NFT{nftsPurchased.length === 1 ? '' : 's'}
</Box>
<Box>{formatEthPrice(totalPurchaseValue.toString())} ETH</Box>
</Row>
<a href={txHashUrl} target="_blank" rel="noreferrer">
<Box color="textPrimary" fontWeight="normal">
{shortenTxHash(txHash, 2, 2)}
</Box>
</a>
</Box>
</Box> </Box>
</Box> </Trace>
)} )}
{/* NFTs that were not purchased ie Refunds */} {/* NFTs that were not purchased ie Refunds */}
{showRefundModal && {showRefundModal &&
/* Showing both purchases & refunds */ /* Showing both purchases & refunds */
(showPurchasedModal ? ( (showPurchasedModal ? (
<Box className={styles.mixedRefundModal} onClick={stopPropagation}> <Trace
<Box name={EventName.NFT_BUY_BAG_REFUNDED}
height="full" properties={{
display="inline-flex" buy_quantity: nftsPurchased.length,
flexWrap="wrap" fail_quantity: nftsNotPurchased.length,
width={{ sm: 'full', md: 'half' }} refund_amount_usd: totalUSDRefund,
paddingRight={{ sm: '0', md: '32' }} transaction_hash: txHash,
> ...trace,
<LightningBoltIcon color="pink" /> }}
<p className={styles.subtitle}>Instant Refund</p> shouldLogImpression
<p className={styles.interStd}> >
Uniswap returned{' '} <Box className={styles.mixedRefundModal} onClick={stopPropagation}>
<span style={{ fontWeight: '700' }}>{formatEthPrice(totalRefundValue.toString())} ETH</span> back
to your wallet for unavailable items.
</p>
<Box <Box
display="flex" height="full"
display="inline-flex"
flexWrap="wrap" flexWrap="wrap"
bottom="24" width={{ sm: 'full', md: 'half' }}
width="full" paddingRight={{ sm: '0', md: '32' }}
alignSelf="flex-end"
position={{ sm: 'absolute', md: 'static' }}
> >
<p className={styles.totalEthCost} style={{ marginBottom: '2px' }}> <LightningBoltIcon color="pink" />
{formatEthPrice(totalRefundValue.toString())} ETH <p className={styles.subtitle}>Instant Refund</p>
</p> <p className={styles.interStd}>
<p className={styles.totalUsdRefund}>{formatUSDPriceWithCommas(totalUSDRefund)}</p> Uniswap returned{' '}
<p className={styles.totalEthCost} style={{ width: '100%' }}> <span style={{ fontWeight: '700' }}>{formatEthPrice(totalRefundValue.toString())} ETH</span>{' '}
for {nftsNotPurchased.length} unavailable item back to your wallet for unavailable items.
{nftsNotPurchased.length === 1 ? '' : 's'}.
</p> </p>
<Box <Box
position={{ sm: 'absolute', md: 'relative' }} display="flex"
right={{ sm: '0', md: 'auto' }} flexWrap="wrap"
bottom={{ sm: '0', md: 'auto' }} bottom="24"
justifyContent={{ sm: 'flex-end', md: 'flex-start' }} width="full"
textAlign={{ sm: 'right', md: 'left' }} alignSelf="flex-end"
flexShrink="0" position={{ sm: 'absolute', md: 'static' }}
marginRight={{ sm: '40', md: '24' }}
width={{ sm: 'half', md: 'auto' }}
> >
<a href={txHashUrl} target="_blank" rel="noreferrer"> <p className={styles.totalEthCost} style={{ marginBottom: '2px' }}>
<Box fontWeight="normal" marginTop="16" className={styles.totalEthCost}> {formatEthPrice(totalRefundValue.toString())} ETH
{shortenTxHash(txHash, 2, 2)} </p>
</Box> <p className={styles.totalUsdRefund}>{formatUSDPriceWithCommas(totalUSDRefund)}</p>
</a> <p className={styles.totalEthCost} style={{ width: '100%' }}>
for {nftsNotPurchased.length} unavailable item
{nftsNotPurchased.length === 1 ? '' : 's'}.
</p>
<Box
position={{ sm: 'absolute', md: 'relative' }}
right={{ sm: '0', md: 'auto' }}
bottom={{ sm: '0', md: 'auto' }}
justifyContent={{ sm: 'flex-end', md: 'flex-start' }}
textAlign={{ sm: 'right', md: 'left' }}
flexShrink="0"
marginRight={{ sm: '40', md: '24' }}
width={{ sm: 'half', md: 'auto' }}
>
<a href={txHashUrl} target="_blank" rel="noreferrer">
<Box fontWeight="normal" marginTop="16" className={styles.totalEthCost}>
{shortenTxHash(txHash, 2, 2)}
</Box>
</a>
</Box>
</Box> </Box>
</Box> </Box>
<Box className={styles.refundAssetsContainer}>
{nftsNotPurchased.map((nft, index) => (
<Box display="flex" flexWrap="wrap" height="min" width="52" key={index}>
<img className={styles.refundAssetImage} src={nft.imageUrl} alt={nft.name} key={index} />
</Box>
))}
</Box>
<Box className={styles.refundOverflowFade} />
</Box> </Box>
<Box className={styles.refundAssetsContainer}> </Trace>
{nftsNotPurchased.map((nft, index) => (
<Box display="flex" flexWrap="wrap" height="min" width="52" key={index}>
<img className={styles.refundAssetImage} src={nft.imageUrl} alt={nft.name} key={index} />
</Box>
))}
</Box>
<Box className={styles.refundOverflowFade} />
</Box>
) : ( ) : (
// Only showing when all assets are unavailable // Only showing when all assets are unavailable
<Box className={styles.fullRefundModal} onClick={stopPropagation}> <Trace
<Box marginLeft="auto" marginRight="auto" display="flex"> name={EventName.NFT_BUY_BAG_REFUNDED}
{txState === TxStateType.Success ? ( properties={{
<> buy_quantity: 0,
<LightningBoltIcon /> fail_quantity: nftsNotPurchased.length,
<h1 className={styles.title}>Instant Refund</h1> refund_amount_usd: totalUSDRefund,
</> ...trace,
) : ( }}
<h1 className={styles.title}>Failed Transaction</h1> shouldLogImpression
)} >
</Box> <Box className={styles.fullRefundModal} onClick={stopPropagation}>
<p className={styles.bodySmall}> <Box marginLeft="auto" marginRight="auto" display="flex">
{txState === TxStateType.Success && {txState === TxStateType.Success ? (
`Selected item${ <>
nftsPurchased.length === 1 ? ' is' : 's are' <LightningBoltIcon />
} no longer available. Uniswap instantly refunded you for this incomplete transaction. `} <h1 className={styles.title}>Instant Refund</h1>
{formatUsdPrice(txFeeFiat)} was used for gas in attempt to complete this transaction. For support, </>
please visit our <a href="https://discord.gg/FCfyBSbCU5">Discord</a> ) : (
</p> <h1 className={styles.title}>Failed Transaction</h1>
<Box className={styles.allUnavailableAssets}> )}
{nftsNotPurchased.length >= 3 && ( </Box>
<Box className={styles.toggleUnavailable} onClick={() => toggleShowUnavailable()}> <p className={styles.bodySmall}>
{!showUnavailable && ( {txState === TxStateType.Success &&
<Box paddingLeft="20" paddingTop="8" paddingBottom="8"> `Selected item${
{nftsNotPurchased.slice(0, 3).map((asset, index) => ( nftsPurchased.length === 1 ? ' is' : 's are'
<img } no longer available. Uniswap instantly refunded you for this incomplete transaction. `}
style={{ zIndex: 2 - index }} {formatUsdPrice(txFeeFiat)} was used for gas in attempt to complete this transaction. For support,
className={styles.unavailableAssetPreview} please visit our <a href="https://discord.gg/FCfyBSbCU5">Discord</a>
src={asset.imageUrl} </p>
alt={asset.name} <Box className={styles.allUnavailableAssets}>
key={index} {nftsNotPurchased.length >= 3 && (
/> <Box className={styles.toggleUnavailable} onClick={() => toggleShowUnavailable()}>
))} {!showUnavailable && (
</Box> <Box paddingLeft="20" paddingTop="8" paddingBottom="8">
)} {nftsNotPurchased.slice(0, 3).map((asset, index) => (
<Box <img
color={showUnavailable ? 'textPrimary' : 'textSecondary'} style={{ zIndex: 2 - index }}
className={styles.unavailableText} className={styles.unavailableAssetPreview}
> src={asset.imageUrl}
Unavailable alt={asset.name}
<Box className={styles.unavailableItems}> key={index}
{nftsNotPurchased.length} item{nftsNotPurchased.length === 1 ? '' : 's'} />
))}
</Box>
)}
<Box
color={showUnavailable ? 'textPrimary' : 'textSecondary'}
className={styles.unavailableText}
>
Unavailable
<Box className={styles.unavailableItems}>
{nftsNotPurchased.length} item{nftsNotPurchased.length === 1 ? '' : 's'}
</Box>
</Box> </Box>
<ChevronUpIcon className={`${!showUnavailable && styles.chevronDown} ${styles.chevron}`} />
</Box> </Box>
<ChevronUpIcon className={`${!showUnavailable && styles.chevronDown} ${styles.chevron}`} /> )}
</Box> {(showUnavailable || nftsNotPurchased.length < 3) &&
)} nftsNotPurchased.map((asset, index) => (
{(showUnavailable || nftsNotPurchased.length < 3) && <Box
nftsNotPurchased.map((asset, index) => ( backgroundColor="backgroundSurface"
<Box display="flex"
backgroundColor="backgroundSurface" padding="4"
display="flex" marginBottom="1"
padding="4" borderRadius="8"
marginBottom="1" key={index}
borderRadius="8" >
key={index} <Box className={styles.assetContainer}>
> <img className={styles.fullRefundImage} src={asset.imageUrl} alt={asset.name} />
<Box className={styles.assetContainer}>
<img className={styles.fullRefundImage} src={asset.imageUrl} alt={asset.name} />
</Box>
<Box flexWrap="wrap" marginTop="4">
<Box marginLeft="4" width="full" display="flex">
<p className={styles.totalEthCost} style={{ marginBottom: '2px' }}>
{formatEthPrice(
asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice
)}{' '}
ETH
</p>
</Box> </Box>
<Box color="textPrimary" className={styles.totalUsdRefund}> <Box flexWrap="wrap" marginTop="4">
{txState === TxStateType.Success ? 'Refunded' : asset.name} <Box marginLeft="4" width="full" display="flex">
<p className={styles.totalEthCost} style={{ marginBottom: '2px' }}>
{formatEthPrice(
asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice
)}{' '}
ETH
</p>
</Box>
<Box color="textPrimary" className={styles.totalUsdRefund}>
{txState === TxStateType.Success ? 'Refunded' : asset.name}
</Box>
</Box> </Box>
</Box> </Box>
</Box> ))}
))} </Box>
</Box> {showUnavailable && <Box className={styles.fullRefundOverflowFade} />}
{showUnavailable && <Box className={styles.fullRefundOverflowFade} />} <p className={styles.totalEthCost} style={{ marginBottom: '2px' }}>
<p className={styles.totalEthCost} style={{ marginBottom: '2px' }}> {formatEthPrice(totalRefundValue.toString())} ETH
{formatEthPrice(totalRefundValue.toString())} ETH </p>
</p> <p className={styles.totalUsdRefund}>{formatUSDPriceWithCommas(totalUSDRefund)}</p>
<p className={styles.totalUsdRefund}>{formatUSDPriceWithCommas(totalUSDRefund)}</p> <Box className={styles.walletAddress} marginLeft="auto" marginRight="0">
<Box className={styles.walletAddress} marginLeft="auto" marginRight="0"> <a href={txHashUrl} target="_blank" rel="noreferrer">
<a href={txHashUrl} target="_blank" rel="noreferrer"> <Box className={styles.addressHash}>{shortenTxHash(txHash, 2, 2)}</Box>
<Box className={styles.addressHash}>{shortenTxHash(txHash, 2, 2)}</Box> </a>
</a> </Box>
</Box> <p className={styles.totalEthCost}>
<p className={styles.totalEthCost}> for {nftsNotPurchased.length} unavailable item
for {nftsNotPurchased.length} unavailable item {nftsNotPurchased.length === 1 ? '' : 's'}.
{nftsNotPurchased.length === 1 ? '' : 's'}. </p>
</p> <Box
<Box as="button"
as="button" border="none"
border="none" backgroundColor="genieBlue"
backgroundColor="genieBlue" cursor="pointer"
cursor="pointer" className={styles.returnButton}
className={styles.returnButton} type="button"
type="button" onClick={() => closeTxCompleteScreen()}
onClick={() => closeTxCompleteScreen()} >
> <BackArrowIcon className={styles.fullRefundBackArrow} />
<BackArrowIcon className={styles.fullRefundBackArrow} /> Return to Marketplace
Return to Marketplace </Box>
</Box> </Box>
</Box> </Trace>
))} ))}
</Box> </Box>
</Portal> </Portal>
......
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, PageName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import clsx from 'clsx' import clsx from 'clsx'
import { MouseoverTooltip } from 'components/Tooltip/index' import { MouseoverTooltip } from 'components/Tooltip/index'
import useENSName from 'hooks/useENSName' import useENSName from 'hooks/useENSName'
...@@ -132,6 +135,15 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -132,6 +135,15 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
const [isOwned, setIsOwned] = useState(false) const [isOwned, setIsOwned] = useState(false)
const { account: address, provider } = useWeb3React() const { account: address, provider } = useWeb3React()
const trace = useTrace({ page: PageName.NFT_DETAILS_PAGE })
const eventProperties = {
collection_address: asset.address,
token_id: asset.tokenId,
token_type: asset.tokenType,
...trace,
}
const { rarityProvider, rarityLogo } = useMemo( const { rarityProvider, rarityLogo } = useMemo(
() => () =>
asset.rarity asset.rarity
...@@ -394,7 +406,10 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -394,7 +406,10 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
onClick={() => { onClick={() => {
if (isSelected) { if (isSelected) {
removeAssetsFromBag([asset]) removeAssetsFromBag([asset])
} else addAssetsToBag([asset]) } else {
addAssetsToBag([asset])
sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { ...eventProperties })
}
setSelected((x) => !x) setSelected((x) => !x)
}} }}
> >
......
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, PageName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons' import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset, TokenType } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, TokenType } from 'nft/types'
...@@ -196,6 +199,14 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) ...@@ -196,6 +199,14 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const USDPrice = useUsdPrice(asset) const USDPrice = useUsdPrice(asset)
const isErc1555 = asset.tokenType === TokenType.ERC1155 const isErc1555 = asset.tokenType === TokenType.ERC1155
const trace = useTrace({ page: PageName.NFT_DETAILS_PAGE })
const eventProperties = {
collection_address: asset.address,
token_id: asset.tokenId,
token_type: asset.tokenType,
...trace,
}
const { quantity, assetInBag } = useMemo(() => { const { quantity, assetInBag } = useMemo(() => {
return { return {
quantity: itemsInBag.filter( quantity: itemsInBag.filter(
...@@ -242,7 +253,10 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) ...@@ -242,7 +253,10 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
assetInBag={assetInBag} assetInBag={assetInBag}
margin={true} margin={true}
useAccentColor={true} useAccentColor={true}
onClick={() => (assetInBag ? removeAssetsFromBag([asset]) : addAssetsToBag([asset]))} onClick={() => {
assetInBag ? removeAssetsFromBag([asset]) : addAssetsToBag([asset])
!assetInBag && sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { ...eventProperties })
}}
> >
<ThemedText.SubHeader lineHeight={'20px'}>{assetInBag ? 'Remove' : 'Buy Now'}</ThemedText.SubHeader> <ThemedText.SubHeader lineHeight={'20px'}>{assetInBag ? 'Remove' : 'Buy Now'}</ThemedText.SubHeader>
</BuyNowButton> </BuyNowButton>
......
...@@ -3,6 +3,8 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -3,6 +3,8 @@ import { BigNumber } from '@ethersproject/bignumber'
import { hexStripZeros } from '@ethersproject/bytes' import { hexStripZeros } from '@ethersproject/bytes'
import { ContractReceipt } from '@ethersproject/contracts' import { ContractReceipt } from '@ethersproject/contracts'
import type { JsonRpcSigner } from '@ethersproject/providers' import type { JsonRpcSigner } from '@ethersproject/providers'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
...@@ -48,6 +50,7 @@ export const useSendTransaction = create<TxState>()( ...@@ -48,6 +50,7 @@ export const useSendTransaction = create<TxState>()(
const res = await signer.sendTransaction(tx) const res = await signer.sendTransaction(tx)
set({ state: TxStateType.Confirming }) set({ state: TxStateType.Confirming })
set({ txHash: res.hash }) set({ txHash: res.hash })
sendAnalyticsEvent(EventName.NFT_BUY_BAG_SIGNED, { transaction_hash: res.hash })
const txReceipt = await res.wait() const txReceipt = await res.wait()
......
...@@ -91,7 +91,7 @@ const Collection = () => { ...@@ -91,7 +91,7 @@ const Collection = () => {
<> <>
<Trace <Trace
page={PageName.NFT_COLLECTION_PAGE} page={PageName.NFT_COLLECTION_PAGE}
properties={{ collection_address: contractAddress, chain_id: chainId }} properties={{ collection_address: contractAddress, chain_id: chainId, is_activity_view: isActivityToggled }}
shouldLogImpression shouldLogImpression
> >
<Column width="full"> <Column width="full">
......
import { GenieAsset } from 'nft/types'
export const formatAssetEventProperties = (assets: GenieAsset[]) => ({
collection_addresses: assets.map((asset) => asset.address),
token_ids: assets.map((asset) => asset.tokenId),
token_types: assets.map((asset) => asset.tokenType),
})
...@@ -5,6 +5,7 @@ export * from './calcPoolPrice' ...@@ -5,6 +5,7 @@ export * from './calcPoolPrice'
export * from './carousel' export * from './carousel'
export * from './currency' export * from './currency'
export * from './fetchPrice' export * from './fetchPrice'
export * from './formatEventProperties'
export * from './isAudio' export * from './isAudio'
export * from './isVideo' export * from './isVideo'
export * from './listNfts' export * from './listNfts'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment