Skip to content

Commit

Permalink
[PAY-1588] Use existing balance in purchase flow on mobile (#3885)
Browse files Browse the repository at this point in the history
  • Loading branch information
dharit-tan committed Aug 14, 2023
1 parent 49d1d05 commit eaef4e6
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 95 deletions.
41 changes: 41 additions & 0 deletions packages/common/src/store/purchase-content/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
import BN from 'bn.js'

import { BNUSDC } from 'models/Wallet'
import { BN_USDC_CENT_WEI, ceilingBNUSDCToNearestCent } from 'utils/wallet'

import { PurchaseContentStage } from './types'

export const zeroBalance = () => new BN(0) as BNUSDC

export const isContentPurchaseInProgress = (stage: PurchaseContentStage) => {
return [
PurchaseContentStage.BUY_USDC,
PurchaseContentStage.PURCHASING,
PurchaseContentStage.CONFIRMING_PURCHASE
].includes(stage)
}

type PurchaseSummaryValues = {
amountDue: number
existingBalance: number | undefined
basePrice: number
artistCut: number
}

export const getPurchaseSummaryValues = (
price: number,
currentBalance: BNUSDC = zeroBalance()
): PurchaseSummaryValues => {
let amountDue = price
let existingBalance
const priceBN = new BN(price).mul(BN_USDC_CENT_WEI)

if (currentBalance.gte(priceBN)) {
amountDue = 0
existingBalance = price
}
// Only count the balance if it's greater than 1 cent
else if (currentBalance.gt(BN_USDC_CENT_WEI)) {
// Note: Rounding amount due *up* to nearest cent for cases where the balance
// is between cents so that we aren't advertising *lower* than what the user
// will have to pay.
const diff = priceBN.sub(currentBalance)
amountDue = ceilingBNUSDCToNearestCent(diff as BNUSDC)
.div(BN_USDC_CENT_WEI)
.toNumber()
existingBalance = price - amountDue
}

return { amountDue, existingBalance, basePrice: price, artistCut: price }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
useGetTrackById,
purchaseContentSelectors,
purchaseContentActions,
PurchaseContentStage
PurchaseContentStage,
combineStatuses,
useUSDCBalance,
getPurchaseSummaryValues,
statusIsNotFinalized
} from '@audius/common'
import { Linking, View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
Expand All @@ -21,6 +25,7 @@ import { flexRowCentered, makeStyles } from 'app/styles'
import { spacing } from 'app/styles/spacing'
import { useThemeColors } from 'app/utils/theme'

import LoadingSpinner from '../loading-spinner/LoadingSpinner'
import { TrackDetailsTile } from '../track-details-tile'

import { PurchaseSuccess } from './PurchaseSuccess'
Expand Down Expand Up @@ -93,24 +98,34 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({
errorContainer: {
...flexRowCentered(),
gap: spacing(2)
},
spinnerContainer: {
width: '100%',
height: '90%',
justifyContent: 'center',
...flexRowCentered()
}
}))

export const PremiumTrackPurchaseDrawer = () => {
const styles = useStyles()
const { neutralLight2, accentRed, secondary } = useThemeColors()
const dispatch = useDispatch()
const isUSDCEnabled = useIsUSDCEnabled()
const { data } = useDrawer('PremiumTrackPurchase')
const { trackId } = data
const { data: track } = useGetTrackById(
const { data: track, status: trackStatus } = useGetTrackById(
{ id: trackId },
{ disabled: !trackId }
)
const isUSDCEnabled = useIsUSDCEnabled()
const { data: currentBalance, status: balanceStatus } = useUSDCBalance()
const error = useSelector(getPurchaseContentError)
const stage = useSelector(getPurchaseContentFlowStage)
const isPurchaseSuccessful = stage === PurchaseContentStage.FINISH
const { premium_conditions: premiumConditions } = track ?? {}
const isLoading = statusIsNotFinalized(
combineStatuses([trackStatus, balanceStatus])
)

const handleClosed = useCallback(() => {
dispatch(purchaseContentActions.cleanup())
Expand All @@ -126,61 +141,73 @@ export const PremiumTrackPurchaseDrawer = () => {
!isUSDCEnabled
)
return null
const price = formatPrice(premiumConditions.usdc_purchase.price)

const price = premiumConditions.usdc_purchase.price
const purchaseSummaryValues = getPurchaseSummaryValues(price, currentBalance)

return (
<NativeDrawer
drawerName={PREMIUM_TRACK_PURCHASE_MODAL_NAME}
onClosed={handleClosed}
>
<View style={styles.drawer}>
<View style={styles.titleContainer}>
<IconCart fill={neutralLight2} />
<Text style={styles.title}>{messages.title}</Text>
{isLoading ? (
<View style={styles.spinnerContainer}>
<LoadingSpinner />
</View>
<TrackDetailsTile trackId={track.track_id} />
<PurchaseSummaryTable
price={price}
isPurchaseSuccessful={isPurchaseSuccessful}
/>
{isPurchaseSuccessful ? (
<PurchaseSuccess />
) : (
<>
<View>
<View style={styles.payToUnlockTitleContainer}>
<Text weight='heavy' textTransform='uppercase' fontSize='small'>
{messages.payToUnlock}
) : (
<View style={styles.drawer}>
<View style={styles.titleContainer}>
<IconCart fill={neutralLight2} />
<Text style={styles.title}>{messages.title}</Text>
</View>
<TrackDetailsTile trackId={track.track_id} />
<PurchaseSummaryTable
{...purchaseSummaryValues}
isPurchaseSuccessful={isPurchaseSuccessful}
/>
{isPurchaseSuccessful ? (
<PurchaseSuccess />
) : (
<>
<View>
<View style={styles.payToUnlockTitleContainer}>
<Text
weight='heavy'
textTransform='uppercase'
fontSize='small'
>
{messages.payToUnlock}
</Text>
<LockedStatusBadge locked />
</View>
<Text>
{messages.disclaimer(
<Text colorValue={secondary} onPress={handleTermsPress}>
{messages.termsOfUse}
</Text>
)}
</Text>
<LockedStatusBadge locked />
</View>
<Text>
{messages.disclaimer(
<Text colorValue={secondary} onPress={handleTermsPress}>
{messages.termsOfUse}
</Text>
)}
<StripePurchaseConfirmationButton
trackId={track.track_id}
price={formatPrice(price)}
/>
</>
)}
{error ? (
<View style={styles.errorContainer}>
<IconError
fill={accentRed}
width={spacing(5)}
height={spacing(5)}
/>
<Text weight='medium' colorValue={accentRed}>
{messages.error}
</Text>
</View>
<StripePurchaseConfirmationButton
trackId={track.track_id}
price={price}
/>
</>
)}
{error ? (
<View style={styles.errorContainer}>
<IconError
fill={accentRed}
width={spacing(5)}
height={spacing(5)}
/>
<Text weight='medium' colorValue={accentRed}>
{messages.error}
</Text>
</View>
) : null}
</View>
) : null}
</View>
)}
</NativeDrawer>
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatPrice } from '@audius/common'
import { View } from 'react-native'

import { Text } from 'app/components/core'
Expand Down Expand Up @@ -48,17 +49,22 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({
}))

type PurchaseSummaryTableProps = {
price: string
amountDue: number
artistCut: number
basePrice: number
existingBalance?: number
isPurchaseSuccessful: boolean
existingBalance?: string
}

export const PurchaseSummaryTable = ({
price,
isPurchaseSuccessful,
existingBalance
amountDue,
artistCut,
basePrice,
existingBalance,
isPurchaseSuccessful
}: PurchaseSummaryTableProps) => {
const styles = useStyles()
const amountDueFormatted = formatPrice(amountDue)

return (
<>
Expand All @@ -74,7 +80,7 @@ export const PurchaseSummaryTable = ({
</View>
<View style={styles.summaryRow}>
<Text>{messages.artistCut}</Text>
<Text>{messages.price(price)}</Text>
<Text>{messages.price(formatPrice(artistCut))}</Text>
</View>
<View style={styles.summaryRow}>
<Text>{messages.audiusCut}</Text>
Expand All @@ -83,7 +89,7 @@ export const PurchaseSummaryTable = ({
{existingBalance ? (
<View style={styles.summaryRow}>
<Text>{messages.existingBalance}</Text>
<Text>{messages.subtractPrice(existingBalance)}</Text>
<Text>{messages.subtractPrice(formatPrice(existingBalance))}</Text>
</View>
) : null}
<View style={[styles.summaryRow, styles.lastRow, styles.greyRow]}>
Expand All @@ -98,15 +104,15 @@ export const PurchaseSummaryTable = ({
color='secondary'
style={styles.strikeThrough}
>
{messages.price(price)}
{messages.price(formatPrice(basePrice))}
</Text>
<Text weight='bold' color='secondary'>
{messages.price('0.50')}
{messages.price(amountDueFormatted)}
</Text>
</>
) : (
<Text weight='bold' color='secondary'>
{messages.price(price)}
{messages.price(amountDueFormatted)}
</Text>
)}
</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCallback } from 'react'

import {
BN_USDC_CENT_WEI,
BNUSDC,
ceilingBNUSDCToNearestCent,
getPurchaseSummaryValues,
ContentType,
isContentPurchaseInProgress,
isPremiumContentUSDCPurchaseGated,
Expand All @@ -13,7 +12,6 @@ import {
UserTrackMetadata
} from '@audius/common'
import { HarmonyButton, IconError } from '@audius/stems'
import BN from 'bn.js'
import { useDispatch, useSelector } from 'react-redux'

import { Icon } from 'components/Icon'
Expand All @@ -24,10 +22,7 @@ import { Text } from 'components/typography'
import { FormatPrice } from './FormatPrice'
import { PayToUnlockInfo } from './PayToUnlockInfo'
import styles from './PurchaseDetailsPage.module.css'
import {
PurchaseSummaryTable,
PurchaseSummaryTableProps
} from './PurchaseSummaryTable'
import { PurchaseSummaryTable } from './PurchaseSummaryTable'

const { startPurchaseContentFlow } = purchaseContentActions
const { getPurchaseContentFlowStage, getPurchaseContentError } =
Expand All @@ -39,35 +34,6 @@ const messages = {
error: 'Your purchase was unsuccessful.'
}

const zeroBalance = () => new BN(0) as BNUSDC

const getPurchaseSummaryValues = (
price: number,
currentBalance: BNUSDC
): PurchaseSummaryTableProps => {
let amountDue = price
let existingBalance
const priceBN = new BN(price).mul(BN_USDC_CENT_WEI)

if (currentBalance.gte(priceBN)) {
amountDue = 0
existingBalance = price
}
// Only count the balance if it's greater than 1 cent
else if (currentBalance.gt(BN_USDC_CENT_WEI)) {
// Note: Rounding amount due *up* to nearest cent for cases where the balance
// is between cents so that we aren't advertising *lower* than what the user
// will have to pay.
const diff = priceBN.sub(currentBalance)
amountDue = ceilingBNUSDCToNearestCent(diff as BNUSDC)
.div(BN_USDC_CENT_WEI)
.toNumber()
existingBalance = price - amountDue
}

return { amountDue, existingBalance, basePrice: price, artistCut: price }
}

const ContentPurchaseError = () => {
return (
<Text className={styles.errorContainer} color='--accent-red'>
Expand All @@ -83,7 +49,7 @@ export type PurchaseDetailsPageProps = {
}

export const PurchaseDetailsPage = ({
currentBalance = zeroBalance(),
currentBalance,
track
}: PurchaseDetailsPageProps) => {
const dispatch = useDispatch()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const messages = {
price: (val: string) => `$${val}`
}

export type PurchaseSummaryTableProps = {
type PurchaseSummaryTableProps = {
amountDue: number
artistCut: number
basePrice: number
Expand Down

0 comments on commit eaef4e6

Please sign in to comment.