Skip to content

Commit

Permalink
[C-2956] Add new Access & Sale modal to legacy upload form (#3900)
Browse files Browse the repository at this point in the history
  • Loading branch information
amendelsohn committed Aug 25, 2023
1 parent 5258675 commit 5bf820c
Show file tree
Hide file tree
Showing 15 changed files with 541 additions and 211 deletions.
9 changes: 7 additions & 2 deletions packages/web/src/common/store/cache/tracks/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { make } from 'common/store/analytics/actions'
import { fetchUsers } from 'common/store/cache/users/sagas'
import * as signOnActions from 'common/store/pages/signon/actions'
import { updateProfileAsync } from 'common/store/profile/sagas'
import { processTracksForUpload } from 'common/store/upload/sagaHelpers'
import { dominantColor } from 'utils/imageProcessingUtil'
import { waitForWrite } from 'utils/sagaHelpers'

Expand Down Expand Up @@ -111,18 +112,22 @@ function* editTrackAsync(action) {
)
}

const [{ metadata: trackForEdit }] = yield processTracksForUpload([
{ metadata: action.formFields }
])

yield call(
confirmEditTrack,
action.trackId,
action.formFields,
trackForEdit,
wasDownloadable,
isNowDownloadable,
wasUnlisted,
isNowListed,
currentTrack
)

const track = { ...action.formFields }
const track = { ...trackForEdit }
track.track_id = action.trackId
if (track.artwork?.file) {
track._cover_art_sizes = {
Expand Down
43 changes: 41 additions & 2 deletions packages/web/src/common/store/upload/sagaHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Name, accountSelectors } from '@audius/common'
import {
BN_USDC_CENT_WEI,
FeatureFlags,
Name,
accountSelectors,
getContext,
getUSDCUserBank,
isPremiumContentUSDCPurchaseGated
} from '@audius/common'
import BN from 'bn.js'
import { range } from 'lodash'
import { all, put, select } from 'typed-redux-saga'
import { all, call, put, select } from 'typed-redux-saga'

import { make } from 'common/store/analytics/actions'
import { TrackForUpload } from 'pages/upload-page/types'
import { waitForWrite } from 'utils/sagaHelpers'
const { getAccountUser } = accountSelectors

Expand Down Expand Up @@ -51,3 +61,32 @@ export function* reportResultEvents({
[...successEvents, ...failureEvents, ...rejectedEvents].map((e) => put(e))
)
}

export function* processTracksForUpload(tracks: TrackForUpload[]) {
const getFeatureEnabled = yield* getContext('getFeatureEnabled')
const isUsdcPurchaseEnabled = yield* call(
getFeatureEnabled,
FeatureFlags.USDC_PURCHASES
)
if (!isUsdcPurchaseEnabled) return tracks

const ownerAccount = yield* select(getAccountUser)
const wallet = ownerAccount?.erc_wallet ?? ownerAccount?.wallet
const ownerUserbank = yield* getUSDCUserBank(wallet)

tracks.forEach((track) => {
const premium_conditions = track.metadata.premium_conditions
if (isPremiumContentUSDCPurchaseGated(premium_conditions)) {
const priceCents = premium_conditions.usdc_purchase.price
const priceWei = new BN(priceCents).mul(BN_USDC_CENT_WEI).toNumber()
premium_conditions.usdc_purchase = {
price: priceCents,
splits: {
[ownerUserbank.toString()]: priceWei
}
}
}
})

return tracks
}
21 changes: 10 additions & 11 deletions packages/web/src/common/store/upload/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import { make } from 'common/store/analytics/actions'
import { getUnclaimedPlaylistId } from 'common/store/cache/collections/utils'
import { trackNewRemixEvent } from 'common/store/cache/tracks/sagas'
import { addPlaylistsNotInLibrary } from 'common/store/playlist-library/sagas'
import {
processTracksForUpload,
reportResultEvents
} from 'common/store/upload/sagaHelpers'
import { updateAndFlattenStems } from 'pages/upload-page/store/utils/stems'
import { ERROR_PAGE } from 'utils/route'
import { waitForWrite } from 'utils/sagaHelpers'
Expand All @@ -44,7 +48,6 @@ import { processAndCacheTracks } from '../cache/tracks/utils'
import { adjustUserField } from '../cache/users/sagas'

import { watchUploadErrors } from './errorSagas'
import { reportResultEvents } from './sagaHelpers'

const { getUser } = cacheUsersSelectors
const { getAccountUser, getUserHandle, getUserId } = accountSelectors
Expand Down Expand Up @@ -1184,22 +1187,18 @@ function* uploadTracksAsync(action) {
})
yield put(recordEvent)

const tracks = yield call(processTracksForUpload, action.tracks)

// Upload content.
const isPlaylist = action.uploadType === UploadType.PLAYLIST
const isAlbum = action.uploadType === UploadType.ALBUM
const isSingleTrack = action.tracks.length === 1
const isSingleTrack = tracks.length === 1
if (isPlaylist || isAlbum) {
yield call(
uploadCollection,
action.tracks,
user.user_id,
action.metadata,
isAlbum
)
yield call(uploadCollection, tracks, user.user_id, action.metadata, isAlbum)
} else if (isSingleTrack) {
yield call(uploadSingleTrack, action.tracks[0])
yield call(uploadSingleTrack, tracks[0])
} else {
yield call(uploadMultipleTracks, action.tracks)
yield call(uploadMultipleTracks, tracks)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.availabilityButton {
margin-top: 18px;
}
249 changes: 249 additions & 0 deletions packages/web/src/components/data-entry/AccessAndSaleModalLegacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { useMemo } from 'react'

import {
Track,
TrackAvailabilityType,
isPremiumContentCollectibleGated,
isPremiumContentFollowGated,
isPremiumContentTipGated,
isPremiumContentUSDCPurchaseGated
} from '@audius/common'
import {
Button,
ButtonSize,
ButtonType,
IconCart,
IconCollectible,
IconHidden,
IconSpecialAccess,
IconVisibilityPublic
} from '@audius/stems'
import { set, isEmpty, get } from 'lodash'
import { z } from 'zod'
import { toFormikValidationSchema } from 'zod-formik-adapter'

import { TrackMetadataState } from 'components/track-availability-modal/types'
import { defaultFieldVisibility } from 'pages/track-page/utils'
import {
AVAILABILITY_TYPE,
AccessAndSaleFormValues,
AccessAndSaleMenuFields,
FIELD_VISIBILITY,
IS_PREMIUM,
IS_UNLISTED,
PREMIUM_CONDITIONS,
PREVIEW,
PRICE,
PRICE_HUMANIZED,
SPECIAL_ACCESS_TYPE
} from 'pages/upload-page/fields/AccessAndSaleField'
import { SpecialAccessType } from 'pages/upload-page/fields/availability/SpecialAccessFields'

import styles from './AccessAndSaleModalLegacy.module.css'
import { ContextualMenu } from './ContextualMenu'

const messages = {
title: 'Access & Sale',
description:
"Customize your music's availability for different audiences, and create personalized gated experiences for your fans.",
public: 'Public (Default)',
premium: 'Premium',
specialAccess: 'Special Access',
collectibleGated: 'Collectible Gated',
hidden: 'Hidden',
errors: {
price: {
tooLow: 'Price must be at least $0.99',
tooHigh: 'Price must be less than $9.99'
},
preview: {
tooEarly: 'Preview must start during the track',
tooLate:
'Preview must start at lest 15 seconds before the end of the track'
}
}
}

const AccessAndSaleFormSchema = (trackLength: number) =>
z.object({
[PREMIUM_CONDITIONS]: z.nullable(
z.object({
// TODO: there are other types
usdc_purchase: z.object({
price: z
.number()
.lte(999, messages.errors.price.tooHigh)
.gte(99, messages.errors.price.tooLow)
})
})
),
[PREVIEW]: z.optional(
z
.number()
.gte(0, messages.errors.preview.tooEarly)
.lte(trackLength - 15, messages.errors.preview.tooLate)
)
})

type AccessAndSaleModalLegacyProps = {
isRemix: boolean
isUpload: boolean
initialForm: Track
metadataState: TrackMetadataState
trackLength: number
didUpdateState: (newState: TrackMetadataState) => void
}

export const AccessAndSaleModalLegacy = (
props: AccessAndSaleModalLegacyProps
) => {
const {
isUpload,
isRemix,
initialForm,
metadataState,
trackLength,
didUpdateState
} = props
const {
premium_conditions: premiumConditions,
unlisted: isUnlisted,
is_premium: isPremium,
preview_start_seconds: preview,
...fieldVisibility
} = metadataState

const initialValues: AccessAndSaleFormValues = useMemo(() => {
const isUsdcGated = isPremiumContentUSDCPurchaseGated(premiumConditions)
const isTipGated = isPremiumContentTipGated(premiumConditions)
const isFollowGated = isPremiumContentFollowGated(premiumConditions)
const isCollectibleGated =
isPremiumContentCollectibleGated(premiumConditions)
const initialValues = {}
set(initialValues, IS_UNLISTED, isUnlisted)
set(initialValues, IS_PREMIUM, isPremium)
set(initialValues, PREMIUM_CONDITIONS, premiumConditions)

let availabilityType = TrackAvailabilityType.PUBLIC
if (isUsdcGated) {
availabilityType = TrackAvailabilityType.USDC_PURCHASE
set(
initialValues,
PRICE_HUMANIZED,
(Number(premiumConditions.usdc_purchase.price || 0) / 100).toFixed(2)
)
}
if (isFollowGated || isTipGated) {
availabilityType = TrackAvailabilityType.SPECIAL_ACCESS
}
if (isCollectibleGated) {
availabilityType = TrackAvailabilityType.COLLECTIBLE_GATED
}
if (isUnlisted) {
availabilityType = TrackAvailabilityType.HIDDEN
}
set(initialValues, AVAILABILITY_TYPE, availabilityType)
set(initialValues, FIELD_VISIBILITY, fieldVisibility)
set(initialValues, PREVIEW, preview)
set(
initialValues,
SPECIAL_ACCESS_TYPE,
isTipGated ? SpecialAccessType.TIP : SpecialAccessType.FOLLOW
)
return initialValues as AccessAndSaleFormValues
}, [fieldVisibility, isPremium, isUnlisted, premiumConditions, preview])

const onSubmit = (values: AccessAndSaleFormValues) => {
let newState = {
...metadataState,
is_premium: !isEmpty(values[PREMIUM_CONDITIONS]),
premium_conditions: values[PREMIUM_CONDITIONS],
unlisted: values.is_unlisted,
preview_start_seconds: values[PREVIEW] ?? 0
}

if (
get(values, AVAILABILITY_TYPE) === TrackAvailabilityType.USDC_PURCHASE
) {
newState.is_premium = true
const price = Math.round(get(values, PRICE))
newState.premium_conditions = {
// @ts-ignore splits get added in saga
usdc_purchase: {
price
}
}
}

if (get(values, AVAILABILITY_TYPE) === TrackAvailabilityType.HIDDEN) {
newState = {
...newState,
...(get(values, FIELD_VISIBILITY) ?? undefined),
unlisted: true
}
} else {
newState = {
...newState,
...defaultFieldVisibility,
unlisted: false
}
}

didUpdateState(newState)
}

let availabilityButtonTitle = messages.public
let AvailabilityIcon = IconVisibilityPublic
if (isUnlisted) {
availabilityButtonTitle = messages.hidden
AvailabilityIcon = IconHidden
} else if (isPremium) {
if (isPremiumContentUSDCPurchaseGated(premiumConditions)) {
availabilityButtonTitle = messages.premium
AvailabilityIcon = IconCart
} else if (isPremiumContentCollectibleGated(premiumConditions)) {
availabilityButtonTitle = messages.collectibleGated
AvailabilityIcon = IconCollectible
} else {
availabilityButtonTitle = messages.specialAccess
AvailabilityIcon = IconSpecialAccess
}
}

return (
<ContextualMenu
label={messages.title}
description={messages.description}
icon={<IconHidden />}
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={toFormikValidationSchema(
// @ts-ignore
AccessAndSaleFormSchema(trackLength)
)}
menuFields={
<AccessAndSaleMenuFields
isRemix={isRemix}
isUpload={isUpload}
isInitiallyUnlisted={initialForm[IS_UNLISTED]}
initialPremiumConditions={
initialForm[PREMIUM_CONDITIONS] ?? undefined
}
premiumConditions={metadataState.premium_conditions}
/>
}
renderValue={() => null}
previewOverride={(toggleMenu) => (
<Button
className={styles.availabilityButton}
type={ButtonType.COMMON_ALT}
name='availabilityModal'
text={availabilityButtonTitle}
size={ButtonSize.SMALL}
onClick={toggleMenu}
leftIcon={<AvailabilityIcon />}
/>
)}
/>
)
}

0 comments on commit 5bf820c

Please sign in to comment.