Skip to content

Commit

Permalink
[PAY-2585] Premium album edit feature (#8071)
Browse files Browse the repository at this point in the history
  • Loading branch information
DejayJD authored Apr 10, 2024
1 parent d3768ff commit 626946d
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 26 deletions.
65 changes: 65 additions & 0 deletions packages/common/src/hooks/useAccessAndRemixSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,69 @@ describe('useAccessAndRemixSettings', () => {
expect(actual).toEqual(expected)
})
})
describe('album edit', () => {
it('public track - should disable all options', () => {
const actual = useAccessAndRemixSettings({
isUpload: false,
isRemix: false,
isAlbum: true,
initialStreamConditions: null,
isInitiallyUnlisted: false,
isScheduledRelease: false
})
const expected = {
disableUsdcGate: true,
disableSpecialAccessGate: true,
disableSpecialAccessGateFields: true,
disableCollectibleGate: true,
disableCollectibleGateFields: true,
disableHidden: true
}
expect(actual).toEqual(expected)
})
it('usdc gated - should disable everything except original option', () => {
mockedUseSelector.mockImplementation(
mockUseSelector(reduxStateWithCollectibles)
)
const actual = useAccessAndRemixSettings({
isUpload: false,
isRemix: false,
isAlbum: true,
initialStreamConditions: mockUSDCGateConditions,
isInitiallyUnlisted: false,
isScheduledRelease: false
})
const expected = {
disableUsdcGate: false,
disableSpecialAccessGate: true,
disableSpecialAccessGateFields: true,
disableCollectibleGate: true,
disableCollectibleGateFields: true,
disableHidden: true
}
expect(actual).toEqual(expected)
})
it('initially hidden - should enable everything', () => {
mockedUseSelector.mockImplementation(
mockUseSelector(reduxStateWithCollectibles)
)
const actual = useAccessAndRemixSettings({
isUpload: false,
isRemix: false,
isAlbum: true,
initialStreamConditions: null,
isInitiallyUnlisted: true,
isScheduledRelease: false
})
const expected = {
disableUsdcGate: false,
disableSpecialAccessGate: true,
disableSpecialAccessGateFields: true,
disableCollectibleGate: true,
disableCollectibleGateFields: true,
disableHidden: true // atm hidden albums are not supported so this is currently disabled
}
expect(actual).toEqual(expected)
})
})
})
9 changes: 5 additions & 4 deletions packages/common/src/hooks/useAccessAndRemixSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ export const useAccessAndRemixSettings = ({
const isInitiallyHidden = !isUpload && isInitiallyUnlisted

const disableUsdcGate =
isRemix ||
isInitiallyPublic ||
isInitiallySpecialAccess ||
isInitiallyCollectibleGated
!isInitiallyUsdcGated &&
(isRemix ||
isInitiallyPublic ||
isInitiallySpecialAccess ||
isInitiallyCollectibleGated)

const disableSpecialAccessGate =
isAlbum ||
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/schemas/upload/uploadFormSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export const createCollectionSchema = (collectionType: 'playlist' | 'album') =>
USDCPurchaseConditionsSchema,
z.object({
usdc_purchase: z.object({
albumTrackPrice: z.number().optional() // Albums can set a price for all tracks
albumTrackPrice: z.number().optional() // Album uploads can set a price for all tracks
})
})
)
Expand Down
6 changes: 6 additions & 0 deletions packages/web/src/components/create-playlist/PlaylistForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { toFormikValidationSchema } from 'zod-formik-adapter'

import { ArtworkField, TextAreaField, TextField } from 'components/form-fields'
import { useCollectionCoverArt } from 'hooks/useCollectionCoverArt'
import { AccessAndSaleField } from 'pages/upload-page/fields/AccessAndSaleField'

import { EditActions } from './FormActions'

Expand Down Expand Up @@ -104,6 +105,11 @@ const PlaylistForm = ({
/>
</Flex>
</Flex>
{isAlbum ? (
<Flex>
<AccessAndSaleField isAlbum />
</Flex>
) : null}
<EditActions
deleteText={
isAlbum
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
gap: var(--harmony-unit-4);
padding: var(--harmony-unit-4) var(--harmony-unit-6);
flex-direction: column;
flex: 1;
cursor: pointer;
}

Expand Down
46 changes: 34 additions & 12 deletions packages/web/src/pages/upload-page/fields/AccessAndSaleField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
USDCPurchaseConditions,
AccessConditions
} from '@audius/common/models'
import { accountSelectors } from '@audius/common/store'
import { CollectionValues } from '@audius/common/schemas'
import { accountSelectors, EditPlaylistValues } from '@audius/common/store'
import { formatPrice, Nullable } from '@audius/common/utils'
import {
IconCart,
Expand All @@ -25,7 +26,7 @@ import {
IconVisibilityPublic,
Text
} from '@audius/harmony'
import { useField } from 'formik'
import { useField, useFormikContext } from 'formik'
import { get, isEmpty, set } from 'lodash'
import { useSelector } from 'react-redux'
import { z } from 'zod'
Expand All @@ -40,7 +41,7 @@ import DynamicImage from 'components/dynamic-image/DynamicImage'
import { defaultFieldVisibility } from 'pages/track-page/utils'

import { useIndexedField, useTrackField } from '../hooks'
import { SingleTrackEditValues } from '../types'
import { SingleTrackEditValues, TrackEditFormValues } from '../types'

import styles from './AccessAndSaleField.module.css'
import { AccessAndSaleMenuFields } from './AccessAndSaleMenuFields'
Expand Down Expand Up @@ -151,7 +152,8 @@ const refineMaxPrice =
export const AccessAndSaleFormSchema = (
trackLength: number,
{ minContentPriceCents, maxContentPriceCents }: USDCPurchaseRemoteConfig,
isAlbum?: boolean
isAlbum?: boolean,
isUpload?: boolean
) =>
z
.object({
Expand All @@ -169,7 +171,7 @@ export const AccessAndSaleFormSchema = (
// Check for albumTrackPrice price >= min price (if applicable)
.refine(
(values) =>
isAlbum
isAlbum && isUpload
? refineMinPrice('albumTrackPrice', minContentPriceCents)(values)
: true,
{
Expand All @@ -184,7 +186,7 @@ export const AccessAndSaleFormSchema = (
})
.refine(
(values) =>
isAlbum
isAlbum && isUpload
? refineMaxPrice('albumTrackPrice', maxContentPriceCents)(values)
: true,
{
Expand Down Expand Up @@ -236,7 +238,7 @@ type AccessAndSaleFieldProps = {
}

export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
const { isUpload, isAlbum, forceOpen, setForceOpen } = props
const { isUpload = false, isAlbum = false, forceOpen, setForceOpen } = props

const [{ value: index }] = useField('trackMetadatasIndex')
const [{ value: trackLength }] = useIndexedField<number>(
Expand All @@ -247,6 +249,16 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {

const usdcPurchaseConfig = useUSDCPurchaseConfig()

// For edit flows we need to track initial stream conditions from the parent form (not from inside contextual menu)
// So we take this from the parent form and pass it down to the menu fields
const { initialValues: parentFormInitialValues } = useFormikContext<
EditPlaylistValues | CollectionValues | TrackEditFormValues
>()
const parentFormInitialStreamConditions =
'stream_conditions' in parentFormInitialValues
? (parentFormInitialValues.stream_conditions as AccessConditions)
: undefined

// Fields from the outer form
const [{ value: isUnlisted }, , { setValue: setIsUnlistedValue }] =
useTrackField<SingleTrackEditValues[typeof IS_UNLISTED]>(IS_UNLISTED)
Expand All @@ -266,6 +278,7 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
useTrackField<SingleTrackEditValues[typeof STREAM_CONDITIONS]>(
STREAM_CONDITIONS
)

const [{ value: fieldVisibility }, , { setValue: setFieldVisibilityValue }] =
useTrackField<SingleTrackEditValues[typeof FIELD_VISIBILITY]>(
FIELD_VISIBILITY
Expand Down Expand Up @@ -549,7 +562,7 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
}
const albumTrackPrice =
savedStreamConditions.usdc_purchase.albumTrackPrice
if (albumTrackPrice) {
if (albumTrackPrice && isUpload) {
selectedValues.push({
label: messages.price(albumTrackPrice / 100),
icon: IconNote,
Expand Down Expand Up @@ -595,11 +608,12 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
</div>
)
}, [
fieldVisibility,
isUnlisted,
savedStreamConditions,
isUnlisted,
isScheduledRelease,
fieldVisibility,
preview,
isScheduledRelease
isUpload
])

return (
Expand All @@ -611,14 +625,22 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
onSubmit={handleSubmit}
renderValue={renderValue}
validationSchema={toFormikValidationSchema(
AccessAndSaleFormSchema(trackLength, usdcPurchaseConfig, isAlbum)
AccessAndSaleFormSchema(
trackLength,
usdcPurchaseConfig,
isAlbum,
isUpload
)
)}
menuFields={
<AccessAndSaleMenuFields
isRemix={isRemix}
isUpload={isUpload}
isAlbum={isAlbum}
streamConditions={tempStreamConditions}
initialStreamConditions={
parentFormInitialStreamConditions ?? undefined
}
isScheduledRelease={isScheduledRelease}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ const messages = {
albumTrackPrice: {
title: 'Track Price',
description:
'Set the price fans must pay to unlock a single track on your album (minimum price of $0.99)',
'Set the price fans must pay to unlock a single track on your album (minimum price of $1.00)',
label: 'Track price',
placeholder: '1.00'
},
// Album purchase flow
albumPrice: {
title: 'Album Price',
description:
'Set the price fans must pay to unlock this album (minimum price of $0.99) ',
'Set the price fans must pay to unlock this album (minimum price of $1.00) ',
label: 'Album price',
placeholder: '5.00'
}
Expand All @@ -75,6 +75,7 @@ export enum UsdcPurchaseType {
export type TrackAvailabilityFieldsProps = {
disabled?: boolean
isAlbum?: boolean
isUpload?: boolean
}

type PriceMessages = typeof messages.price
Expand All @@ -84,7 +85,7 @@ export type PriceFieldProps = TrackAvailabilityFieldsProps & {
}

export const UsdcPurchaseFields = (props: TrackAvailabilityFieldsProps) => {
const { disabled, isAlbum } = props
const { disabled, isAlbum, isUpload } = props
const [{ value: downloadConditions }] =
useField<Nullable<AccessConditions>>(DOWNLOAD_CONDITIONS)

Expand All @@ -97,11 +98,13 @@ export const UsdcPurchaseFields = (props: TrackAvailabilityFieldsProps) => {
messaging={messages.price.albumPrice}
fieldName={PRICE}
/>
<PriceField
disabled={disabled}
messaging={messages.price.albumTrackPrice}
fieldName={ALBUM_TRACK_PRICE}
/>
{isUpload && (
<PriceField
disabled={disabled}
messaging={messages.price.albumTrackPrice}
fieldName={ALBUM_TRACK_PRICE}
/>
)}
<input type='hidden' name={PREVIEW} value='0' />
{downloadConditions && !isAlbum ? (
<HelpCallout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ export const UsdcPurchaseGatedRadioField = (
hintContent={!isUsdcUploadEnabled ? helpContent : undefined}
tag={!isUsdcUploadEnabled ? messages.comingSoon : undefined}
checkedContent={
<UsdcPurchaseFields disabled={disabled} isAlbum={isAlbum} />
<UsdcPurchaseFields
disabled={disabled}
isAlbum={isAlbum}
isUpload={isUpload}
/>
}
/>
)
Expand Down

0 comments on commit 626946d

Please sign in to comment.