Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

[PAY-1573] Convert premium content type to union #3711

Merged
merged 7 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions packages/common/src/hooks/usePremiumContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { useMemo } from 'react'

import { useSelector } from 'react-redux'

import { Chain, ID, PremiumConditions, Track } from 'models'
import {
Chain,
ID,
PremiumConditions,
Track,
isPremiumContentCollectibleGated,
isPremiumContentFollowGated,
isPremiumContentTipGated
} from 'models'
import { getAccountUser } from 'store/account/selectors'
import { cacheTracksSelectors, cacheUsersSelectors } from 'store/cache'
import { premiumContentSelectors } from 'store/premium-content'
Expand All @@ -29,7 +37,9 @@ export const usePremiumContentAccess = (track: Nullable<Partial<Track>>) => {
const hasPremiumContentSignature =
!!track.premium_content_signature ||
!!(trackId && premiumTrackSignatureMap[trackId])
const isCollectibleGated = !!track.premium_conditions?.nft_collection
const isCollectibleGated = isPremiumContentCollectibleGated(
track.premium_conditions
)
const isSignatureToBeFetched =
isCollectibleGated &&
!!trackId &&
Expand Down Expand Up @@ -67,7 +77,9 @@ export const usePremiumContentAccessMap = (tracks: Partial<Track>[]) => {
const hasPremiumContentSignature = !!(
track.premium_content_signature || premiumTrackSignatureMap[trackId]
)
const isCollectibleGated = !!track.premium_conditions?.nft_collection
const isCollectibleGated = isPremiumContentCollectibleGated(
track.premium_conditions
)
const isSignatureToBeFetched =
isCollectibleGated &&
premiumTrackSignatureMap[trackId] === undefined &&
Expand All @@ -88,11 +100,15 @@ export const usePremiumContentAccessMap = (tracks: Partial<Track>[]) => {
export const usePremiumConditionsEntity = (
premiumConditions: Nullable<PremiumConditions>
) => {
const {
follow_user_id: followUserId,
tip_user_id: tipUserId,
nft_collection: nftCollection
} = premiumConditions ?? {}
const followUserId = isPremiumContentFollowGated(premiumConditions)
? premiumConditions?.follow_user_id
: null
const tipUserId = isPremiumContentTipGated(premiumConditions)
? premiumConditions?.tip_user_id
: null
const nftCollection = isPremiumContentCollectibleGated(premiumConditions)
? premiumConditions?.nft_collection
: null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ternaries here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's because of ?? {} ; if we had a branch that checked if premiumConditions was null first and then returned early we'd probably be good, tho would be tricky given hooks here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still getting type errors even if I null check first. gonna stick with the type guard for now but let's chat about it tmr.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can live with the type guards!

const users = useSelector((state: CommonState) =>
getUsers(state, {
Expand Down
54 changes: 44 additions & 10 deletions packages/common/src/models/Track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type RemixOf = {
tracks: Remix[]
}

// Premium content
export type TokenStandard = 'ERC721' | 'ERC1155'

export type PremiumConditionsEthNFTCollection = {
Expand All @@ -75,20 +76,53 @@ export type PremiumConditionsSolNFTCollection = {
externalLink: Nullable<string>
}

export type PremiumConditionsUSDCPurchase = {
price: StringUSDC
slot: number
}

export type PremiumConditions = {
nft_collection?:
// nft_collection can be undefined during upload flow when user has set track to
// collectible-gated but hasn't specified collection yet, but should always be defined
// after user has set the collection.
export type PremiumConditionsCollectibleGated = {
nft_collection:
| PremiumConditionsEthNFTCollection
| PremiumConditionsSolNFTCollection
follow_user_id?: number
tip_user_id?: number
usdc_purchase?: PremiumConditionsUSDCPurchase
| undefined
}

export type PremiumConditionsFollowGated = { follow_user_id: number }

export type PremiumConditionsTipGated = { tip_user_id: number }

export type PremiumConditionsUSDCPurchase = {
usdc_purchase: {
price: StringUSDC
slot: number
}
}

export type PremiumConditions =
| PremiumConditionsCollectibleGated
| PremiumConditionsFollowGated
| PremiumConditionsTipGated
| PremiumConditionsUSDCPurchase

export const isPremiumContentCollectibleGated = (
premiumConditions?: Nullable<PremiumConditions>
): premiumConditions is PremiumConditionsCollectibleGated =>
'nft_collection' in (premiumConditions ?? {})

export const isPremiumContentFollowGated = (
premiumConditions?: Nullable<PremiumConditions>
): premiumConditions is PremiumConditionsFollowGated =>
'follow_user_id' in (premiumConditions ?? {})

export const isPremiumContentTipGated = (
premiumConditions?: Nullable<PremiumConditions>
): premiumConditions is PremiumConditionsTipGated =>
'tip_user_id' in (premiumConditions ?? {})

export const isPremiumContentUSDCPurchaseGated = (
premiumConditions?: Nullable<PremiumConditions>
): premiumConditions is PremiumConditionsUSDCPurchase =>
'usdc_purchase' in (premiumConditions ?? {})

export type PremiumContentSignature = {
data: string
signature: string
Expand Down
44 changes: 22 additions & 22 deletions packages/common/src/store/premium-content/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import {
PremiumContentSignature,
PremiumTrackStatus,
Track,
TrackMetadata
TrackMetadata,
isPremiumContentCollectibleGated,
isPremiumContentFollowGated,
isPremiumContentTipGated
} from 'models'
import { User } from 'models/User'
import { FeatureFlags, IntKeys } from 'services/remote-config'
import { IntKeys } from 'services/remote-config'
import { accountSelectors } from 'store/account'
import { cacheActions, cacheTracksSelectors } from 'store/cache'
import { collectiblesActions } from 'store/collectibles'
Expand Down Expand Up @@ -136,7 +139,11 @@ function* getTokenIdMap({
// skip this track entry if it is not premium or if it is not gated on an nft collection
const { is_premium: isPremium, premium_conditions: premiumConditions } =
tracks[trackId]
if (!isPremium || !premiumConditions || !premiumConditions.nft_collection)
if (
!isPremium ||
!premiumConditions ||
!isPremiumContentCollectibleGated(premiumConditions)
)
return

// Set the token ids for ERC1155 nfts as the balanceOf contract method
Expand All @@ -155,7 +162,7 @@ function* getTokenIdMap({
)
}

if (nftCollection.chain === Chain.Eth) {
if (nftCollection?.chain === Chain.Eth) {
// skip this track entry if user does not own an nft from its nft collection gate
const tokenIds = ethContractMap[nftCollection.address]
if (!tokenIds || !tokenIds.length) return
Expand All @@ -166,7 +173,7 @@ function* getTokenIdMap({
nftCollection.standard === 'ERC1155'
? ethContractMap[nftCollection.address]
: []
} else if (nftCollection.chain === Chain.Sol) {
} else if (nftCollection?.chain === Chain.Sol) {
if (solCollectionMintSet.has(nftCollection.address)) {
// add trackId to trackMap, no need for tokenIds here
trackMap[trackId] = []
Expand Down Expand Up @@ -233,8 +240,8 @@ function* handleSpecialAccessTrackSubscriptions(tracks: Track[]) {
}

const hasNoSignature = !premiumContentSignature
const isFollowGated = !!premiumConditions?.follow_user_id
const isTipGated = !!premiumConditions?.tip_user_id
const isFollowGated = isPremiumContentFollowGated(premiumConditions)
const isTipGated = isPremiumContentTipGated(premiumConditions)
const shouldHaveSignature =
(isFollowGated && followeeIds.includes(ownerId)) ||
(isTipGated && tippedUserIds.includes(ownerId))
Expand Down Expand Up @@ -338,16 +345,6 @@ function* updateGatedTrackAccess(
| ReturnType<typeof cacheActions.addSucceeded>
| ReturnType<typeof cacheActions.update>
) {
// Halt if premium content not enabled
const getFeatureEnabled = yield* getContext('getFeatureEnabled')
const isGatedContentEnabled = yield* call(
getFeatureEnabled,
FeatureFlags.GATED_CONTENT_ENABLED
)
if (!isGatedContentEnabled) {
return
}

const account = yield* select(getAccountUser)

// Halt if nfts fetched are not for logged in account
Expand Down Expand Up @@ -483,9 +480,12 @@ function* pollPremiumTrack({
yield* put(showConfetti())
}

const eventName = track.premium_conditions?.follow_user_id
if (!track.premium_conditions) {
return
}
const eventName = isPremiumContentFollowGated(track.premium_conditions)
? Name.FOLLOW_GATED_TRACK_UNLOCKED
: track.premium_conditions?.tip_user_id
: isPremiumContentTipGated(track.premium_conditions)
? Name.TIP_GATED_TRACK_UNLOCKED
: null
if (eventName) {
Expand Down Expand Up @@ -544,8 +544,8 @@ function* updateSpecialAccessTracks(
} = cachedTracks[id]
const isGated =
gate === 'follow'
? premiumConditions?.follow_user_id
: premiumConditions?.tip_user_id
? isPremiumContentFollowGated(premiumConditions)
: isPremiumContentTipGated(premiumConditions)
if (isGated && ownerId === trackOwnerId) {
statusMap[id] = 'UNLOCKING'
trackParamsMap[id] = parseTrackRouteFromPermalink(permalink)
Expand Down Expand Up @@ -588,7 +588,7 @@ function* handleUnfollowUser(
const id = parseInt(trackId)
const { owner_id: ownerId, premium_conditions: premiumConditions } =
cachedTracks[id]
const isFollowGated = premiumConditions?.follow_user_id
const isFollowGated = isPremiumContentFollowGated(premiumConditions)
if (isFollowGated && ownerId === action.userId) {
statusMap[id] = 'LOCKED'
}
Expand Down
11 changes: 6 additions & 5 deletions packages/common/src/utils/dogEarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export const getDogEarType = ({

// Show premium variants for track owners or if user does not yet have access
if ((isOwner || !doesUserHaveAccess) && premiumConditions != null) {
if (premiumConditions.usdc_purchase) {
if ('usdc_purchase' in premiumConditions) {
return DogEarType.USDC_PURCHASE
}
if (premiumConditions.nft_collection) {
} else if ('nft_collection' in premiumConditions) {
return DogEarType.COLLECTIBLE_GATED
}
if (premiumConditions.follow_user_id || premiumConditions.tip_user_id) {
} else if (
'follow_user_id' in premiumConditions ||
'tip_user_id' in premiumConditions
) {
return DogEarType.SPECIAL_ACCESS
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { ID, PremiumConditions } from '@audius/common'
import { removeNullable, cacheUsersSelectors } from '@audius/common'
import {
isPremiumContentCollectibleGated,
isPremiumContentFollowGated,
isPremiumContentTipGated
} from '@audius/common'
import type { ViewStyle } from 'react-native'
import { useSelector } from 'react-redux'

import { DetailsTileHasAccess } from './DetailsTileHasAccess'
import { DetailsTileNoAccess } from './DetailsTileNoAccess'

const { getUsers } = cacheUsersSelectors

type DetailsTilePremiumAccessProps = {
trackId: ID
premiumConditions: PremiumConditions
Expand All @@ -23,17 +24,10 @@ export const DetailsTilePremiumAccess = ({
doesUserHaveAccess,
style
}: DetailsTilePremiumAccessProps) => {
const { follow_user_id: followUserId, tip_user_id: tipUserId } =
premiumConditions ?? {}
const users = useSelector((state) =>
getUsers(state, {
ids: [followUserId, tipUserId].filter(removeNullable)
})
)
const followee = followUserId ? users[followUserId] : null
const tippedUser = tipUserId ? users[tipUserId] : null
const shouldDisplay =
!!premiumConditions.nft_collection || followee || tippedUser
isPremiumContentCollectibleGated(premiumConditions) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eek, somehow made it more verbose.
I would have thought that if our type is a union of types that are all valid, we just need to check if it's not null here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good point. Probably good to keep it this way for now tho bc usdc_purchase is a valid option but I haven't written the logic to exclude that yet?

isPremiumContentFollowGated(premiumConditions) ||
isPremiumContentTipGated(premiumConditions)

if (!shouldDisplay) return null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { ID, PremiumConditions } from '@audius/common'
import { premiumContentSelectors } from '@audius/common'
import {
isPremiumContentUSDCPurchaseGated,
premiumContentSelectors
} from '@audius/common'
import { View } from 'react-native'
import { useSelector } from 'react-redux'

Expand Down Expand Up @@ -57,7 +60,7 @@ export const LineupTileAccessStatus = ({
<View
style={[
styles.root,
isUSDCEnabled && premiumConditions.usdc_purchase
isUSDCEnabled && isPremiumContentUSDCPurchaseGated(premiumConditions)
? styles.usdcPurchase
: null
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useMemo } from 'react'

import type { PremiumConditions } from '@audius/common'
import {
isPremiumContentCollectibleGated,
isPremiumContentUSDCPurchaseGated,
type PremiumConditions
} from '@audius/common'
import { View } from 'react-native'

import IconCart from 'app/assets/images/iconCart.svg'
Expand Down Expand Up @@ -57,9 +61,9 @@ export const LineupTilePremiumContentTypeTag = ({
const isUSDCEnabled = useIsUSDCEnabled()

const type =
isUSDCEnabled && premiumConditions?.usdc_purchase
isUSDCEnabled && isPremiumContentUSDCPurchaseGated(premiumConditions)
? PremiumContentType.USDC_PURCHASE
: premiumConditions?.nft_collection
: isPremiumContentCollectibleGated(premiumConditions)
? PremiumContentType.COLLECTIBLE_GATED
: PremiumContentType.SPECIAL_ACCESS

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
import {
formatCount,
repostsUserListActions,
favoritesUserListActions
favoritesUserListActions,
isPremiumContentUSDCPurchaseGated
} from '@audius/common'
import { View, TouchableOpacity } from 'react-native'
import { useDispatch } from 'react-redux'
Expand Down Expand Up @@ -210,7 +211,7 @@ export const LineupTileStats = ({
styles.listenCount,
doesUserHaveAccess ? styles.iconUnlocked : null,
isUSDCEnabled &&
premiumConditions.usdc_purchase &&
isPremiumContentUSDCPurchaseGated(premiumConditions) &&
doesUserHaveAccess
? styles.iconUSDC
: null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
useLockedContent,
premiumContentActions,
SquareSizes,
usePremiumContentAccess
usePremiumContentAccess,
isPremiumContentCollectibleGated
} from '@audius/common'
import { Dimensions, View } from 'react-native'
import { useDispatch } from 'react-redux'
Expand Down Expand Up @@ -105,7 +106,9 @@ type TrackDetailsProps = {
const TrackDetails = ({ track, owner }: TrackDetailsProps) => {
const styles = useStyles()
const accentBlue = useColor('accentBlue')
const isCollectibleGated = !!track.premium_conditions?.nft_collection
const isCollectibleGated = isPremiumContentCollectibleGated(
track.premium_conditions
)

return (
<View style={styles.trackDetails}>
Expand Down
Loading