Skip to content

Commit

Permalink
[PAY-1707] Implements usage of existing balance during content purcha…
Browse files Browse the repository at this point in the history
…ses (#3883)
  • Loading branch information
schottra committed Aug 14, 2023
1 parent 4e74cc3 commit 063de8f
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 34 deletions.
1 change: 1 addition & 0 deletions apps/audius-client/packages/common/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './useDebouncedCallback'
export * from './useSavedCollections'
export * from './chats'
export * from './useGeneratePlaylistArtwork'
export * from './useUSDCBalance'
36 changes: 36 additions & 0 deletions apps/audius-client/packages/common/src/hooks/useUSDCBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react'

import BN from 'bn.js'

import { Status } from 'models/Status'
import { BNUSDC } from 'models/Wallet'
import { getUserbankAccountInfo } from 'services/index'
import { useAppContext } from 'src/context/appContext'

/**
* On mount, fetches the USDC balance for the current user
*/
export const useUSDCBalance = () => {
const { audiusBackend } = useAppContext()
const [status, setStatus] = useState(Status.IDLE)
const [data, setData] = useState<BNUSDC>()

useEffect(() => {
const fetch = async () => {
setStatus(Status.LOADING)
try {
const account = await getUserbankAccountInfo(audiusBackend, {
mint: 'usdc'
})
const balance = (account?.amount ?? new BN(0)) as BNUSDC
setData(balance)
setStatus(Status.SUCCESS)
} catch (e) {
setStatus(Status.ERROR)
}
}
fetch()
}, [audiusBackend])

return { status, data }
}
16 changes: 16 additions & 0 deletions apps/audius-client/packages/common/src/models/Status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,19 @@ export enum Status {
export function statusIsNotFinalized(status: Status) {
return [Status.IDLE, Status.LOADING].includes(status)
}

/**
* Reduces an array of `Status` values to the least-complete status or `ERROR` if present.
*/
export function combineStatuses(statuses: Status[]) {
if (statuses.includes(Status.ERROR)) {
return Status.ERROR
}
if (statuses.includes(Status.LOADING)) {
return Status.LOADING
}
if (statuses.includes(Status.IDLE)) {
return Status.IDLE
}
return Status.SUCCESS
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AudiusLibs } from '@audius/sdk'
import { u64 } from '@solana/spl-token'
import { AccountInfo, u64 } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'
import BN from 'bn.js'

Expand Down Expand Up @@ -55,7 +55,7 @@ export const getTokenAccountInfo = async (
mint?: MintName
tokenAccount: PublicKey
}
) => {
): Promise<AccountInfo | null> => {
return (
await audiusBackendInstance.getAudiusLibs()
).solanaWeb3Manager!.getTokenAccountInfo(tokenAccount.toString(), mint)
Expand Down Expand Up @@ -106,6 +106,35 @@ function isCreateUserBankIfNeededError(
return 'error' in res
}

/**
* Returns the userbank account info for the given address and mint. If the
* userbank does not exist, returns null.
*/
export const getUserbankAccountInfo = async (
audiusBackendInstance: AudiusBackend,
{ ethAddress: sourceEthAddress, mint = DEFAULT_MINT }: UserBankConfig = {}
): Promise<AccountInfo | null> => {
const audiusLibs: AudiusLibs = await audiusBackendInstance.getAudiusLibs()
const ethAddress =
sourceEthAddress ?? audiusLibs.Account!.getCurrentUser()?.wallet

if (!ethAddress) {
throw new Error(
`getUserbankAccountInfo: unexpected error getting eth address`
)
}

const tokenAccount = await deriveUserBankPubkey(audiusBackendInstance, {
ethAddress,
mint
})

return getTokenAccountInfo(audiusBackendInstance, {
tokenAccount,
mint
})
}

/**
* Attempts to create a userbank if one does not exist.
* Defaults to AUDIO mint and the current user's wallet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ function* purchaseStep({
tokenAccount
}
)
if (!initialAccountInfo) {
throw new Error('Could not get userbank account info')
}
const initialBalance = initialAccountInfo.amount

yield* put(purchaseStarted())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Name } from 'models/Analytics'
import { ErrorLevel } from 'models/ErrorReporting'
import { ID } from 'models/Identifiers'
import { isPremiumContentUSDCPurchaseGated } from 'models/Track'
import { BNUSDC } from 'models/Wallet'
import {
getTokenAccountInfo,
purchaseContent
Expand All @@ -23,7 +24,7 @@ import { getTrack } from 'store/cache/tracks/selectors'
import { getUser } from 'store/cache/users/selectors'
import { getContext } from 'store/effects'
import { setVisibility } from 'store/ui/modals/slice'
import { BN_USDC_CENT_WEI } from 'utils/wallet'
import { BN_USDC_CENT_WEI, ceilingBNUSDCToNearestCent } from 'utils/wallet'

import { pollPremiumTrack } from '../premium-content/sagas'
import { updatePremiumTrackStatus } from '../premium-content/slice'
Expand Down Expand Up @@ -149,23 +150,34 @@ function* doStartPurchaseContentFlow({
// get user bank
const userBank = yield* call(getUSDCUserBank)

const { amount: initialBalance } = yield* call(
const tokenAccountInfo = yield* call(
getTokenAccountInfo,
audiusBackendInstance,
{
mint: 'usdc',
tokenAccount: userBank
}
)
if (!tokenAccountInfo) {
throw new Error('Failed to fetch USDC token account info')
}

const { amount: initialBalance } = tokenAccountInfo

const priceBN = new BN(price).mul(BN_USDC_CENT_WEI)
const balanceNeeded: BNUSDC = priceBN.sub(initialBalance) as BNUSDC

// buy USDC if necessary
if (initialBalance.lt(new BN(price).mul(BN_USDC_CENT_WEI))) {
if (balanceNeeded.gtn(0)) {
const balanceNeededCents = ceilingBNUSDCToNearestCent(balanceNeeded)
.div(BN_USDC_CENT_WEI)
.toNumber()
yield* put(buyUSDC())
yield* put(
onrampOpened({
provider: USDCOnRampProvider.STRIPE,
purchaseInfo: {
desiredAmount: price
desiredAmount: balanceNeededCents
}
})
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useCallback } from 'react'

import {
combineStatuses,
premiumContentSelectors,
purchaseContentActions,
useGetTrackById
statusIsNotFinalized,
useGetTrackById,
useUSDCBalance
} from '@audius/common'
import { IconCart, Modal, ModalContentPages, ModalHeader } from '@audius/stems'
import { useDispatch, useSelector } from 'react-redux'
Expand All @@ -27,21 +30,33 @@ enum PurchaseSteps {
DETAILS = 1
}

export const PremiumContentPurchaseModal = () => {
const [isOpen, setIsOpen] = useModalState('PremiumContentPurchase')
const usePremiumContentPurchaseModalState = () => {
const trackId = useSelector(getPurchaseContentId)
const dispatch = useDispatch()
const { data: track } = useGetTrackById(
const [isOpen, setIsOpen] = useModalState('PremiumContentPurchase')
const { data: balance, status: balanceStatus } = useUSDCBalance()
const { data: track, status: trackStatus } = useGetTrackById(
{ id: trackId! },
{ disabled: !trackId }
)

const status = combineStatuses([balanceStatus, trackStatus])

const handleClose = useCallback(() => {
setIsOpen(false)
dispatch(purchaseContentActions.cleanup())
}, [setIsOpen, dispatch])

const currentStep = !track ? PurchaseSteps.LOADING : PurchaseSteps.DETAILS
const currentStep = statusIsNotFinalized(status)
? PurchaseSteps.LOADING
: PurchaseSteps.DETAILS

return { isOpen, handleClose, currentStep, track, balance, status }
}

export const PremiumContentPurchaseModal = () => {
const { balance, isOpen, handleClose, currentStep, track } =
usePremiumContentPurchaseModalState()

return (
<Modal
Expand All @@ -65,7 +80,7 @@ export const PremiumContentPurchaseModal = () => {
{track ? (
<ModalContentPages currentPage={currentStep}>
<LoadingPage />
<PurchaseDetailsPage track={track} />
<PurchaseDetailsPage track={track} currentBalance={balance} />
</ModalContentPages>
) : null}
</Modal>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.container {
display: inline-flex;
align-items: center;
gap: var(--unit-2);
}

.existingBalance {
text-decoration: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { formatPrice } from '@audius/common'
import cn from 'classnames'

import styles from './FormatPrice.module.css'

type FormatPriceProps = {
amountDue: number
basePrice: number
className?: string
}

export const FormatPrice = ({
amountDue,
basePrice,
className
}: FormatPriceProps) => {
if (basePrice === amountDue)
return (
<span className={cn(styles.container, className)}>{`$${formatPrice(
amountDue
)}`}</span>
)
return (
<span className={cn(styles.container, className)}>
<del>{`$${formatPrice(basePrice)}`}</del>
<ins className={styles.existingBalance}>{`$${
amountDue === 0 ? '0' : formatPrice(amountDue)
}`}</ins>
</span>
)
}

0 comments on commit 063de8f

Please sign in to comment.