Skip to content

Commit

Permalink
[PAY-1592] Wire up USDC purchase flow on mobile (#3881)
Browse files Browse the repository at this point in the history
  • Loading branch information
dharit-tan committed Aug 14, 2023
1 parent 41cc4af commit 5b4242f
Show file tree
Hide file tree
Showing 17 changed files with 429 additions and 145 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/store/purchase-content/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const slice = createSlice({
state.stage = PurchaseContentStage.START
state.error = undefined
state.contentId = action.payload.contentId
state.contentType = action.payload.contentType || ContentType.TRACK
state.contentType = action.payload.contentType ?? ContentType.TRACK
state.onSuccess = action.payload.onSuccess
},
buyUSDC: (state) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/store/ui/stripe-modal/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ function* handleStripeSessionChanged({
if (onrampSucceeded) {
yield* put(onrampSucceeded)
}
yield* put(setVisibility({ modal: 'StripeOnRamp', visible: false }))
yield* put(setVisibility({ modal: 'StripeOnRamp', visible: 'closing' }))
}
}

function* handleCancelStripeOnramp() {
const { onrampCanceled } = yield* select(getStripeModalState)
yield* put(setVisibility({ modal: 'StripeOnRamp', visible: false }))
yield* put(setVisibility({ modal: 'StripeOnRamp', visible: 'closing' }))

if (onrampCanceled) {
yield* put(onrampCanceled)
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/store/ui/stripe-modal/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { CommonState } from 'store/commonStore'

export const getStripeModalState = (state: CommonState) => state.ui.stripeModal

export const getStripeClientSecret = (state: CommonState) =>
state.ui.stripeModal.stripeClientSecret
2 changes: 1 addition & 1 deletion packages/mobile/.env.stage
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ CLAIM_DISTRIBUTION_CONTRACT_ADDRESS=0x74b89B916c97d50557E8F944F32662fE52Ce378d
SOLANA_WEB3_CLUSTER=mainnet-beta
SOLANA_CLUSTER_ENDPOINT=https://audius-fe.rpcpool.com
WAUDIO_MINT_ADDRESS=BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo
USDC_MINT_ADDRESS=4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
USDC_MINT_ADDRESS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
SOLANA_TOKEN_PROGRAM_ADDRESS=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
CLAIMABLE_TOKEN_PDA=Aw5AjygeMf9Nvg61BXvFSAzkqxcLqL8koepb14kvfc3W
SOLANA_FEE_PAYER_ADDRESS=E3CfijtAJwBSHfwFEViAUd3xp7c8TBxwC1eXn1Fgxp8h
Expand Down
4 changes: 3 additions & 1 deletion packages/mobile/src/app/Drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { RateCtaDrawer } from 'app/components/rate-cta-drawer'
import { ShareDrawer } from 'app/components/share-drawer'
import { ShareToTikTokDrawer } from 'app/components/share-to-tiktok-drawer'
import { SignOutConfirmationDrawer } from 'app/components/sign-out-confirmation-drawer'
import { StripeOnrampDrawer } from 'app/components/stripe-onramp-drawer'
import { SupportersInfoDrawer } from 'app/components/supporters-info-drawer'
import { TransferAudioMobileDrawer } from 'app/components/transfer-audio-mobile-drawer'
import { TrendingRewardsDrawer } from 'app/components/trending-rewards-drawer'
Expand Down Expand Up @@ -107,7 +108,8 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = {
VipDiscord: VipDiscordDrawer,
ProfileActions: ProfileActionsDrawer,
PlaybackRate: PlaybackRateDrawer,
PublishPlaylistConfirmation: PublishPlaylistDrawer
PublishPlaylistConfirmation: PublishPlaylistDrawer,
StripeOnRamp: StripeOnrampDrawer
}

const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { useCallback, type ReactNode } from 'react'

import {
formatPrice,
isPremiumContentUSDCPurchaseGated,
useGetTrackById
useGetTrackById,
purchaseContentSelectors,
purchaseContentActions,
PurchaseContentStage
} from '@audius/common'
import { View } from 'react-native'
import { Linking, View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'

import IconCart from 'app/assets/images/iconCart.svg'
import IconError from 'app/assets/images/iconError.svg'
import { LockedStatusBadge, Text } from 'app/components/core'
import { NativeDrawer } from 'app/components/drawer'
import { useDrawer } from 'app/hooks/useDrawer'
import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled'
import { flexRowCentered, makeStyles } from 'app/styles'
import { useColor } from 'app/utils/theme'
import { spacing } from 'app/styles/spacing'
import { useThemeColors } from 'app/utils/theme'

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

import { PurchaseSuccess } from './PurchaseSuccess'
import { PurchaseSummaryTable } from './PurchaseSummaryTable'
import { StripePurchaseConfirmationButton } from './StripePurchaseConfirmationButton'

const { getPurchaseContentError, getPurchaseContentFlowStage } =
purchaseContentSelectors

const PREMIUM_TRACK_PURCHASE_MODAL_NAME = 'PremiumTrackPurchase'

const messages = {
Expand All @@ -25,16 +39,27 @@ const messages = {
audiusCut: 'Audius Cut',
alwaysZero: 'Always $0',
youPay: 'You Pay',
youPaid: 'You Paid',
price: (price: string) => `$${price}`,
payToUnlock: 'Pay-To-Unlock',
disclaimer:
'By clicking on "Buy", you agree to our Terms of Use. Your purchase will be made in USDC via 3rd party payment provider. Additional payment provider fees may apply. Any remaining USDC balance in your Audius wallet will be applied to this transaction. Once your payment is confirmed, your premium content will be unlocked and available to stream.'
disclaimer: (termsOfUse: ReactNode) => (
<>
{'By clicking on "Buy", you agree to our '}
{termsOfUse}
{
' Your purchase will be made in USDC via 3rd party payment provider. Additional payment provider fees may apply. Any remaining USDC balance in your Audius wallet will be applied to this transaction. Once your payment is confirmed, your premium content will be unlocked and available to stream.'
}
</>
),
termsOfUse: 'Terms of Use.',
error: 'Your purchase was unsuccessful.'
}

const useStyles = makeStyles(({ spacing, typography, palette }) => ({
drawer: {
paddingVertical: spacing(6),
paddingTop: spacing(6),
paddingHorizontal: spacing(4),
paddingBottom: spacing(8),
gap: spacing(6),
backgroundColor: palette.white
},
Expand All @@ -58,94 +83,103 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({
borderRadius: spacing(2),
backgroundColor: palette.neutralLight10
},
summaryContainer: {
borderColor: palette.neutralLight8,
borderWidth: 1,
borderRadius: spacing(1)
},
summaryRow: {
...flexRowCentered(),
justifyContent: 'space-between',
paddingVertical: spacing(3),
paddingHorizontal: spacing(6),
borderBottomColor: palette.neutralLight8,
borderBottomWidth: 1
},
lastRow: {
borderBottomWidth: 0
},
greyRow: {
backgroundColor: palette.neutralLight10
},
summaryTitle: {
letterSpacing: 1
},
payToUnlockTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: spacing(2),
marginBottom: spacing(2)
},
errorContainer: {
...flexRowCentered(),
gap: spacing(2)
}
}))

export const PremiumTrackPurchaseDrawer = () => {
const styles = useStyles()
const neutralLight2 = useColor('neutralLight2')
const { neutralLight2, accentRed, secondary } = useThemeColors()
const dispatch = useDispatch()
const { data } = useDrawer('PremiumTrackPurchase')
const { trackId } = data
const { data: track } = useGetTrackById(
{ id: trackId },
{ disabled: !trackId }
)
const isUSDCEnabled = useIsUSDCEnabled()
const error = useSelector(getPurchaseContentError)
const stage = useSelector(getPurchaseContentFlowStage)
const isPurchaseSuccessful = stage === PurchaseContentStage.FINISH
const { premium_conditions: premiumConditions } = track ?? {}
if (!track || !isPremiumContentUSDCPurchaseGated(premiumConditions))

const handleClosed = useCallback(() => {
dispatch(purchaseContentActions.cleanup())
}, [dispatch])

const handleTermsPress = useCallback(() => {
Linking.openURL('https://audius.co/legal/terms-of-use')
}, [])

if (
!track ||
!isPremiumContentUSDCPurchaseGated(premiumConditions) ||
!isUSDCEnabled
)
return null
const price = formatPrice(premiumConditions.usdc_purchase.price)

return (
<NativeDrawer drawerName={PREMIUM_TRACK_PURCHASE_MODAL_NAME}>
<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>
</View>
<TrackDetailsTile trackId={track.track_id} />
<View style={styles.summaryContainer}>
<View style={[styles.summaryRow, styles.greyRow]}>
<Text
weight='bold'
textTransform='uppercase'
style={styles.summaryTitle}
>
{messages.summary}
<PurchaseSummaryTable
price={price}
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>
</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>
<View style={styles.summaryRow}>
<Text>{messages.artistCut}</Text>
<Text>{messages.price(price)}</Text>
</View>
<View style={styles.summaryRow}>
<Text>{messages.audiusCut}</Text>
<Text>{messages.alwaysZero}</Text>
</View>
<View style={[styles.summaryRow, styles.lastRow, styles.greyRow]}>
<Text weight='bold'>{messages.youPay}</Text>
<Text weight='bold' color='secondary'>
{messages.price(price)}
</Text>
</View>
</View>
<View>
<View style={styles.payToUnlockTitleContainer}>
<Text weight='heavy' textTransform='uppercase' fontSize='small'>
{messages.payToUnlock}
</Text>
<LockedStatusBadge locked />
</View>
<Text>{messages.disclaimer}</Text>
</View>
<StripePurchaseConfirmationButton price={price} />
) : null}
</View>
</NativeDrawer>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { View } from 'react-native'

import IconVerified from 'app/assets/images/iconVerified.svg'
import { Text } from 'app/components/core'
import { flexRowCentered, makeStyles } from 'app/styles'
import { spacing } from 'app/styles/spacing'
import { useThemeColors } from 'app/utils/theme'

import { TwitterButton } from '../twitter-button'

const messages = {
success: 'Your purchase was successful!'
}

const useStyles = makeStyles(({ spacing, typography, palette }) => ({
root: {
paddingTop: spacing(2),
gap: spacing(9),
alignSelf: 'center'
},
successContainer: {
...flexRowCentered(),
alignSelf: 'center',
gap: spacing(2)
}
}))

export const PurchaseSuccess = () => {
const styles = useStyles()
const { specialLightGreen1, staticWhite } = useThemeColors()

return (
<View style={styles.root}>
<View style={styles.successContainer}>
<IconVerified
height={spacing(4)}
width={spacing(4)}
fill={specialLightGreen1}
fillSecondary={staticWhite}
/>
<Text weight='bold'>{messages.success}</Text>
</View>
<TwitterButton
fullWidth
size='large'
type='static'
shareText={messages.success}
/>
</View>
)
}

0 comments on commit 5b4242f

Please sign in to comment.