Skip to content

Commit

Permalink
[PAY-2823] Edit Album access control fixes (#8298)
Browse files Browse the repository at this point in the history
  • Loading branch information
amendelsohn committed May 2, 2024
1 parent b42b6fd commit 7e0aa7e
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 27 deletions.
4 changes: 2 additions & 2 deletions packages/common/src/hooks/useAccessAndRemixSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ describe('useAccessAndRemixSettings', () => {
disableSpecialAccessGateFields: true,
disableCollectibleGate: true,
disableCollectibleGateFields: true,
disableHidden: true
disableHidden: false
}
expect(actual).toEqual(expected)
})
Expand Down Expand Up @@ -393,7 +393,7 @@ describe('useAccessAndRemixSettings', () => {
disableSpecialAccessGateFields: true,
disableCollectibleGate: true,
disableCollectibleGateFields: true,
disableHidden: true
disableHidden: false
}
expect(actual).toEqual(expected)
})
Expand Down
24 changes: 15 additions & 9 deletions packages/common/src/hooks/useAccessAndRemixSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type UseAccessAndRemixSettingsProps = {
initialStreamConditions: Nullable<AccessConditions>
isInitiallyUnlisted: boolean
isScheduledRelease?: boolean
isPublishDisabled?: boolean
}

export const useHasNoCollectibles = () => {
Expand Down Expand Up @@ -47,7 +48,8 @@ export const useAccessAndRemixSettings = ({
isAlbum = false,
initialStreamConditions,
isInitiallyUnlisted,
isScheduledRelease = false
isScheduledRelease = false,
isPublishDisabled = false
}: UseAccessAndRemixSettingsProps) => {
const isInitiallyPublic =
!isInitiallyUnlisted && !isUpload && !initialStreamConditions
Expand All @@ -71,20 +73,23 @@ export const useAccessAndRemixSettings = ({
isContentCollectibleGated(initialStreamConditions)

const isInitiallyHidden = !isUpload && isInitiallyUnlisted
const shouldDisablePublish = isPublishDisabled && isInitiallyHidden

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

const disableSpecialAccessGate =
isAlbum ||
isRemix ||
isInitiallyPublic ||
isInitiallyUsdcGated ||
isInitiallyCollectibleGated
isInitiallyCollectibleGated ||
shouldDisablePublish

// This applies when the parent field is active but we still want to disable sub-options
// used for edit flow to not allow increasing permission strictness
Expand All @@ -99,15 +104,16 @@ export const useAccessAndRemixSettings = ({
isInitiallyPublic ||
isInitiallyUsdcGated ||
isInitiallySpecialAccess ||
hasNoCollectibles
hasNoCollectibles ||
shouldDisablePublish

// This applies when the parent field is active but we still want to disable sub-options
// used for edit flow to not allow increasing permission strictness
const disableCollectibleGateFields =
disableCollectibleGate || (!isUpload && !isInitiallyHidden)

const disableHidden =
isAlbum || isScheduledRelease || (!isUpload && !isInitiallyUnlisted)
isScheduledRelease || (!isUpload && !isInitiallyUnlisted)

return {
disableUsdcGate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ def validate_update_access_conditions(params: ManageEntityParameters):
existing_conditions = existing_playlist.get("stream_conditions")
updated_conditions = updated_playlist.get("stream_conditions")

if existing_playlist.get("is_private"):
# private playlist can be changed to gated or public
return

if not existing_conditions:
# non gated playlist cannot be updated to be gated
if updated_conditions:
Expand Down
15 changes: 14 additions & 1 deletion packages/web/src/common/store/cache/collections/commonSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
User,
UserFollowees,
FolloweeRepost,
UID
UID,
isContentUSDCPurchaseGated
} from '@audius/common/models'
import { TransactionReceipt } from '@audius/common/services'
import {
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
addPlaylistsNotInLibrary,
removePlaylistFromLibrary
} from 'common/store/playlist-library/sagas'
import { getUSDCMetadata } from 'common/store/upload/sagaHelpers'
import { ensureLoggedIn } from 'common/utils/ensureLoggedIn'
import { waitForWrite } from 'utils/sagaHelpers'

Expand Down Expand Up @@ -126,6 +128,17 @@ function* editPlaylistAsync(
.filter(removeNullable)
})

// If the collection is a newly premium album, this will populate the premium metadata (price/splits/etc)
if (
playlist.is_album &&
isContentUSDCPurchaseGated(playlist.stream_conditions)
) {
playlist.stream_conditions = yield* call(
getUSDCMetadata,
playlist.stream_conditions
)
}

// Optimistic update #1 to quickly update metadata and track lineup
yield* call(optimisticUpdateCollection, playlist)

Expand Down
8 changes: 7 additions & 1 deletion packages/web/src/components/create-playlist/PlaylistForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ const PlaylistForm = ({
description: metadata.description ?? ''
}

const hasNoHiddenTracks =
metadata.tracks?.every((track) => track.is_unlisted === false) ?? false

return (
<Formik<EditPlaylistValues>
initialValues={initialValues}
Expand Down Expand Up @@ -108,7 +111,10 @@ const PlaylistForm = ({
</Flex>
{isAlbum ? (
<Flex>
<AccessAndSaleField isAlbum />
<AccessAndSaleField
isAlbum
isPublishDisabled={metadata.is_private && !hasNoHiddenTracks}
/>
</Flex>
) : null}
<EditActions
Expand Down
32 changes: 23 additions & 9 deletions packages/web/src/pages/upload-page/fields/AccessAndSaleField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
GateKeeper,
IS_DOWNLOADABLE,
IS_DOWNLOAD_GATED,
IS_PRIVATE,
IS_SCHEDULED_RELEASE,
IS_STREAM_GATED,
IS_UNLISTED,
Expand Down Expand Up @@ -235,10 +236,19 @@ type AccessAndSaleFieldProps = {
trackLength?: number
forceOpen?: boolean
setForceOpen?: (value: boolean) => void
isPublishDisabled?: boolean
}

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

const isHiddenFieldName = isAlbum ? IS_PRIVATE : IS_UNLISTED

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

// Fields from the outer form
const [{ value: isUnlisted }, , { setValue: setIsUnlistedValue }] =
useTrackField<SingleTrackEditValues[typeof IS_UNLISTED]>(IS_UNLISTED)
useTrackField<boolean>(isHiddenFieldName)
const [{ value: isScheduledRelease }, ,] =
useTrackField<SingleTrackEditValues[typeof IS_SCHEDULED_RELEASE]>(
IS_SCHEDULED_RELEASE
Expand Down Expand Up @@ -388,6 +398,7 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
const fieldVisibility = get(values, FIELD_VISIBILITY)
const streamConditions = get(values, STREAM_CONDITIONS)
const lastGateKeeper = get(values, LAST_GATE_KEEPER)
const isUnlisted = get(values, IS_UNLISTED)

setFieldVisibilityValue({
...defaultFieldVisibility,
Expand Down Expand Up @@ -493,7 +504,6 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
[
setFieldVisibilityValue,
setIsUnlistedValue,
isUnlisted,
setIsStreamGated,
setStreamConditionsValue,
setPreviewValue,
Expand Down Expand Up @@ -573,14 +583,16 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
selectedValues = [specialAccessValue, messages.followersOnly]
} else if (isContentTipGated(savedStreamConditions)) {
selectedValues = [specialAccessValue, messages.supportersOnly]
} else if (isUnlisted && !isScheduledRelease && fieldVisibility) {
} else if (isUnlisted && !isScheduledRelease) {
const fieldVisibilityKeys = Object.keys(
messages.fieldVisibility
) as Array<keyof FieldVisibility>

const fieldVisibilityLabels = fieldVisibilityKeys
.filter((visibilityKey) => fieldVisibility[visibilityKey])
.map((visibilityKey) => messages.fieldVisibility[visibilityKey])
const fieldVisibilityLabels = fieldVisibility
? fieldVisibilityKeys
.filter((visibilityKey) => fieldVisibility[visibilityKey])
.map((visibilityKey) => messages.fieldVisibility[visibilityKey])
: []
selectedValues = [
{ label: messages.hidden, icon: IconHidden },
...fieldVisibilityLabels
Expand Down Expand Up @@ -611,9 +623,9 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
savedStreamConditions,
isUnlisted,
isScheduledRelease,
fieldVisibility,
preview,
isUpload
isUpload,
fieldVisibility
])

return (
Expand Down Expand Up @@ -642,6 +654,8 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
parentFormInitialStreamConditions ?? undefined
}
isScheduledRelease={isScheduledRelease}
isInitiallyUnlisted={isUnlisted}
isPublishDisabled={isPublishDisabled}
/>
}
forceOpen={forceOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ const messages = {
"Hidden tracks won't be visible to your followers. Only you will see them on your profile. Anyone who has the link will be able to listen.",
hiddenSubtitleAlbums:
'Hidden albums remain invisible to your followers, visible only to you on your profile. They can be shared and listened to via direct link.',
hiddenHint: 'Scheduled tracks are hidden by default until release.'
hiddenHint: 'Scheduled tracks are hidden by default until release.',
publishDisabled:
'Publishing is disabled for empty albums and albums containing hidden tracks.'
}

export type AccesAndSaleMenuFieldsProps = {
Expand All @@ -57,6 +59,7 @@ export type AccesAndSaleMenuFieldsProps = {
isInitiallyUnlisted?: boolean
isScheduledRelease?: boolean
initialStreamConditions?: AccessConditions | undefined
isPublishDisabled?: boolean
}

export const AccessAndSaleMenuFields = (props: AccesAndSaleMenuFieldsProps) => {
Expand All @@ -66,7 +69,8 @@ export const AccessAndSaleMenuFields = (props: AccesAndSaleMenuFieldsProps) => {
isAlbum,
isInitiallyUnlisted,
initialStreamConditions,
isScheduledRelease
isScheduledRelease,
isPublishDisabled = false
} = props

const { isEnabled: isUsdcFlagUploadEnabled } = useFeatureFlag(
Expand Down Expand Up @@ -102,19 +106,24 @@ export const AccessAndSaleMenuFields = (props: AccesAndSaleMenuFieldsProps) => {
isAlbum,
initialStreamConditions: initialStreamConditions ?? null,
isInitiallyUnlisted: !!isInitiallyUnlisted,
isScheduledRelease: !!isScheduledRelease
isScheduledRelease: !!isScheduledRelease,
isPublishDisabled
})

return (
<div className={cn(layoutStyles.col, layoutStyles.gap4)}>
{isRemix ? <HelpCallout content={messages.isRemix} /> : null}
<Text variant='body'>{messages.modalDescription}</Text>
{isPublishDisabled ? (
<HelpCallout content={messages.publishDisabled} />
) : null}
<RadioGroup {...availabilityField} aria-label={messages.title}>
<ModalRadioItem
icon={<IconVisibilityPublic className={styles.icon} />}
label={messages.public}
description={messages.publicSubtitle}
value={StreamTrackAvailabilityType.PUBLIC}
disabled={isPublishDisabled}
/>
{isUsdcUploadEnabled ? (
<UsdcPurchaseGatedRadioField
Expand All @@ -123,6 +132,7 @@ export const AccessAndSaleMenuFields = (props: AccesAndSaleMenuFieldsProps) => {
isAlbum={isAlbum}
initialStreamConditions={initialStreamConditions}
isInitiallyUnlisted={isInitiallyUnlisted}
isPublishDisabled={isPublishDisabled}
/>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type UsdcPurchaseGatedRadioFieldProps = {
isAlbum?: boolean
initialStreamConditions?: AccessConditions
isInitiallyUnlisted?: boolean
isPublishDisabled?: boolean
}

export const UsdcPurchaseGatedRadioField = (
Expand All @@ -44,7 +45,8 @@ export const UsdcPurchaseGatedRadioField = (
isUpload,
isAlbum,
initialStreamConditions,
isInitiallyUnlisted
isInitiallyUnlisted,
isPublishDisabled
} = props

const handleClickWaitListLink = useCallback(() => {
Expand All @@ -60,7 +62,8 @@ export const UsdcPurchaseGatedRadioField = (
isRemix,
isAlbum,
initialStreamConditions: initialStreamConditions ?? null,
isInitiallyUnlisted: !!isInitiallyUnlisted
isInitiallyUnlisted: !!isInitiallyUnlisted,
isPublishDisabled
})
const disabled = disableUsdcGate || !isUsdcUploadEnabled

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/pages/upload-page/fields/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const DOWNLOAD_PRICE_HUMANIZED = 'download_price_humanized'
export const STEMS = 'stems'
export const SPECIAL_ACCESS_TYPE = 'special_access_type'
export const IS_UNLISTED = 'is_unlisted'
export const IS_PRIVATE = 'is_private'
export const FIELD_VISIBILITY = 'field_visibility'
export const IS_SCHEDULED_RELEASE = 'is_scheduled_release'
// whether Access & Sale or Stems & Downloads last set the stream / download conditions
Expand Down

0 comments on commit 7e0aa7e

Please sign in to comment.