diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index 32e2d49f089..a01b4d0f90f 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -19,6 +19,7 @@ import { EditCollectiblesDrawer } from 'app/components/edit-collectibles-drawer' import { EnablePushNotificationsDrawer } from 'app/components/enable-push-notifications-drawer' import { FeedFilterDrawer } from 'app/components/feed-filter-drawer' import { ForgotPasswordDrawer } from 'app/components/forgot-password-drawer' +import { GatedContentUploadPromptDrawer } from 'app/components/gated-content-upload-prompt-drawer/GatedContentUploadPromptDrawer' import { LockedContentDrawer } from 'app/components/locked-content-drawer' import { OverflowMenuDrawer } from 'app/components/overflow-menu-drawer' import { PlaybackRateDrawer } from 'app/components/playback-rate-drawer' @@ -115,6 +116,7 @@ const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = { RemoveDownloadedFavorites: RemoveDownloadedFavoritesDrawer, UnfavoriteDownloadedCollection: UnfavoriteDownloadedCollectionDrawer, LockedContent: LockedContentDrawer, + GatedContentUploadPrompt: GatedContentUploadPromptDrawer, ChatActions: ChatActionsDrawer, BlockMessages: BlockMessagesDrawer } diff --git a/packages/mobile/src/assets/images/iconRocket.svg b/packages/mobile/src/assets/images/iconRocket.svg new file mode 100644 index 00000000000..88accca541c --- /dev/null +++ b/packages/mobile/src/assets/images/iconRocket.svg @@ -0,0 +1,12 @@ + + + iconRocket + + + + + + + + + \ No newline at end of file diff --git a/packages/mobile/src/components/gated-content-upload-prompt-drawer/GatedContentUploadPromptDrawer.tsx b/packages/mobile/src/components/gated-content-upload-prompt-drawer/GatedContentUploadPromptDrawer.tsx new file mode 100644 index 00000000000..344505fb4a3 --- /dev/null +++ b/packages/mobile/src/components/gated-content-upload-prompt-drawer/GatedContentUploadPromptDrawer.tsx @@ -0,0 +1,147 @@ +import { useCallback } from 'react' + +import { TouchableOpacity, View } from 'react-native' +import { useDispatch } from 'react-redux' + +import IconArrow from 'app/assets/images/iconArrow.svg' +import IconExternalLink from 'app/assets/images/iconExternalLink.svg' +import IconRocket from 'app/assets/images/iconRocket.svg' +import { Button, Text, useLink } from 'app/components/core' +import { NativeDrawer } from 'app/components/drawer' +import { useNavigation } from 'app/hooks/useNavigation' +import { setVisibility } from 'app/store/drawers/slice' +import { flexRowCentered, makeStyles } from 'app/styles' +import { useColor } from 'app/utils/theme' + +const LEARN_MORE_URL = + 'https://blog.audius.co/guide-to-audius-availability-settings' + +const messages = { + title: 'NEW UPDATE!', + subtitle: 'Control who has access to your tracks!', + description: + 'Availability settings allow you to limit access to specific groups of users or offer exclusive content to your most dedicated fans.', + learnMore: 'Learn More', + gotIt: 'Got It', + checkItOut: 'Check It Out' +} + +const useStyles = makeStyles(({ spacing, palette, typography }) => ({ + drawer: { + marginVertical: spacing(8), + marginHorizontal: spacing(4), + alignItems: 'flex-start' + }, + titleContainer: { + ...flexRowCentered(), + justifyContent: 'center', + width: '100%', + paddingBottom: spacing(4), + marginBottom: spacing(6), + borderBottomWidth: 1, + borderBottomColor: palette.neutralLight8 + }, + titleText: { + marginLeft: spacing(3), + fontFamily: typography.fontByWeight.heavy, + fontSize: typography.fontSize.medium, + color: palette.neutralLight4 + }, + subtitle: { + marginBottom: spacing(6), + fontFamily: typography.fontByWeight.bold, + fontSize: typography.fontSize.large + }, + description: { + marginBottom: spacing(6), + fontFamily: typography.fontByWeight.medium, + fontSize: typography.fontSize.large, + lineHeight: spacing(7) + }, + button: { + marginBottom: spacing(6) + }, + buttonText: { + fontSize: typography.fontSize.large + }, + learnMore: { + ...flexRowCentered(), + marginBottom: spacing(6) + }, + learnMoreIcon: { + marginLeft: spacing(1) + } +})) + +export const GatedContentUploadPromptDrawer = ({ + isUpload +}: { + isUpload?: boolean +}) => { + const styles = useStyles() + const neutralLight4 = useColor('neutralLight4') + const navigation = useNavigation() + const dispatch = useDispatch() + const { onPress: onLearnMorePress } = useLink(LEARN_MORE_URL) + + const handleClose = useCallback(() => { + dispatch( + setVisibility({ drawer: 'GatedContentUploadPrompt', visible: false }) + ) + }, [dispatch]) + + const handleSubmit = useCallback(() => { + handleClose() + navigation.push('Availability') + }, [handleClose, navigation]) + + if (!isUpload) return null + + return ( + + + + + + {messages.title} + + + {messages.subtitle} + {messages.description} + + + {messages.learnMore} + + + + + + + + ) +} diff --git a/packages/mobile/src/components/gated-content-upload-prompt-drawer/index.ts b/packages/mobile/src/components/gated-content-upload-prompt-drawer/index.ts new file mode 100644 index 00000000000..77a9ffbd863 --- /dev/null +++ b/packages/mobile/src/components/gated-content-upload-prompt-drawer/index.ts @@ -0,0 +1 @@ +export { GatedContentUploadPromptDrawer } from './GatedContentUploadPromptDrawer' diff --git a/packages/mobile/src/hooks/useOneTimeDrawer.ts b/packages/mobile/src/hooks/useOneTimeDrawer.ts new file mode 100644 index 00000000000..b9ae5661281 --- /dev/null +++ b/packages/mobile/src/hooks/useOneTimeDrawer.ts @@ -0,0 +1,33 @@ +import { useEffect } from 'react' + +import AsyncStorage from '@react-native-async-storage/async-storage' +import { useDispatch } from 'react-redux' +import { useAsync } from 'react-use' + +import type { Drawer as DrawerName } from 'app/store/drawers/slice' +import { setVisibility } from 'app/store/drawers/slice' + +type UseOneTimeDrawerProps = { + key: string // AsyncStorage key + name: DrawerName + disabled?: boolean +} + +export const useOneTimeDrawer = ({ + key, + name, + disabled = false +}: UseOneTimeDrawerProps) => { + const dispatch = useDispatch() + const { value: seen, loading } = useAsync(() => AsyncStorage.getItem(key)) + + useEffect(() => { + if (disabled) return + + const shouldOpen = !loading && !seen + if (shouldOpen) { + dispatch(setVisibility({ drawer: name, visible: true })) + AsyncStorage.setItem(key, 'true') + } + }, [disabled, loading, seen, dispatch, name, key]) +} diff --git a/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx b/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx index 0f922496765..6492c67f1de 100644 --- a/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx +++ b/packages/mobile/src/screens/edit-track-screen/EditTrackForm.tsx @@ -10,7 +10,10 @@ import IconCaretRight from 'app/assets/images/iconCaretRight.svg' import IconUpload from 'app/assets/images/iconUpload.svg' import { Button, Tile } from 'app/components/core' import { InputErrorMessage } from 'app/components/core/InputErrorMessage' +import { useIsGatedContentEnabled } from 'app/hooks/useIsGatedContentEnabled' +import { useIsSpecialAccessEnabled } from 'app/hooks/useIsSpecialAccessEnabled' import { useNavigation } from 'app/hooks/useNavigation' +import { useOneTimeDrawer } from 'app/hooks/useOneTimeDrawer' import { setVisibility } from 'app/store/drawers/slice' import { makeStyles } from 'app/styles' @@ -36,6 +39,9 @@ const messages = { fixErrors: 'Fix Errors To Continue' } +const GATED_CONTENT_UPLOAD_PROMPT_DRAWER_SEEN_KEY = + 'gated_content_upload_prompt_drawer_seen' + const useStyles = makeStyles(({ spacing }) => ({ backButton: { transform: [{ rotate: '180deg' }], @@ -70,6 +76,15 @@ export const EditTrackForm = (props: EditTrackFormProps) => { const navigation = useNavigation() const dispatch = useDispatch() + const isGatedContentEnabled = useIsGatedContentEnabled() + const isSpecialAccessEnabled = useIsSpecialAccessEnabled() + + useOneTimeDrawer({ + key: GATED_CONTENT_UPLOAD_PROMPT_DRAWER_SEEN_KEY, + name: 'GatedContentUploadPrompt', + disabled: !isGatedContentEnabled || !isSpecialAccessEnabled + }) + const handlePressBack = useCallback(() => { if (!dirty) { navigation.goBack() diff --git a/packages/mobile/src/screens/edit-track-screen/EditTrackNavigator.tsx b/packages/mobile/src/screens/edit-track-screen/EditTrackNavigator.tsx index e9056aebcb1..a3c0042c805 100644 --- a/packages/mobile/src/screens/edit-track-screen/EditTrackNavigator.tsx +++ b/packages/mobile/src/screens/edit-track-screen/EditTrackNavigator.tsx @@ -1,5 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' +import { GatedContentUploadPromptDrawer } from 'app/components/gated-content-upload-prompt-drawer' import { useIsGatedContentEnabled } from 'app/hooks/useIsGatedContentEnabled' import { useAppScreenOptions } from 'app/screens/app-screen/useAppScreenOptions' @@ -28,27 +29,39 @@ export const EditTrackNavigator = (props: EditTrackNavigatorProps) => { const screenOptions = useAppScreenOptions(screenOptionOverrides) return ( - - - {() => } - - - - - - {isGatedContentEnabled ? ( - - ) : ( + <> + + + {() => } + + + + - )} - {isGatedContentEnabled && ( - - )} - - - + {isGatedContentEnabled ? ( + + ) : ( + + )} + {isGatedContentEnabled && ( + + )} + + + + + > ) } diff --git a/packages/mobile/src/screens/edit-track-screen/components/CollectibleGatedAvailability.tsx b/packages/mobile/src/screens/edit-track-screen/components/CollectibleGatedAvailability.tsx index 26eccfb200a..a470d90f3ce 100644 --- a/packages/mobile/src/screens/edit-track-screen/components/CollectibleGatedAvailability.tsx +++ b/packages/mobile/src/screens/edit-track-screen/components/CollectibleGatedAvailability.tsx @@ -7,10 +7,10 @@ import { View, Image, Dimensions } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' import { useSelector } from 'react-redux' -import IconArrow from 'app/assets/images/iconArrow.svg' import IconCaretRight from 'app/assets/images/iconCaretRight.svg' import IconCollectible from 'app/assets/images/iconCollectible.svg' -import { Link, Text } from 'app/components/core' +import IconExternalLink from 'app/assets/images/iconExternalLink.svg' +import { Text, useLink } from 'app/components/core' import { HelpCallout } from 'app/components/help-callout/HelpCallout' import { useNavigation } from 'app/hooks/useNavigation' import { useSetTrackAvailabilityFields } from 'app/hooks/useSetTrackAvailabilityFields' @@ -73,10 +73,10 @@ const useStyles = makeStyles(({ typography, spacing, palette }) => ({ alignItems: 'center' }, learnMoreText: { - marginRight: spacing(0.5), + marginRight: spacing(1), fontFamily: typography.fontByWeight.bold, - fontSize: typography.fontSize.small, - color: palette.secondary + fontSize: typography.fontSize.large, + color: palette.neutralLight4 }, collectionContainer: { marginTop: spacing(4), @@ -129,6 +129,9 @@ const useStyles = makeStyles(({ typography, spacing, palette }) => ({ marginRight: spacing(4), width: spacing(5), height: spacing(5) + }, + learnMoreIcon: { + marginLeft: spacing(1) } })) @@ -148,6 +151,7 @@ export const CollectibleGatedAvailability = ({ const secondary = useColor('secondary') const neutral = useColor('neutral') const neutralLight4 = useColor('neutralLight4') + const { onPress: onLearnMorePress } = useLink(LEARN_MORE_URL) const titleStyles: object[] = [styles.title] if (selected) { @@ -237,10 +241,17 @@ export const CollectibleGatedAvailability = ({ content={renderHelpCalloutContent()} /> ) : null} - - {messages.learnMore} - - + + + {messages.learnMore} + + + {selected && ( void initialValues: ExtendedTrackMetadata & { trackArtwork?: string } doneText?: string + isUpload?: boolean } & Partial export type EditTrackFormProps = FormikProps & Partial & { doneText?: string + isUpload?: boolean } export type RemixOfField = Nullable<{ tracks: { parent_track_id }[] }> diff --git a/packages/mobile/src/screens/upload-screen/screens/CompleteTrackScreen.tsx b/packages/mobile/src/screens/upload-screen/screens/CompleteTrackScreen.tsx index c6cd7292a01..4ed606183d7 100644 --- a/packages/mobile/src/screens/upload-screen/screens/CompleteTrackScreen.tsx +++ b/packages/mobile/src/screens/upload-screen/screens/CompleteTrackScreen.tsx @@ -36,6 +36,7 @@ export const CompleteTrackScreen = () => { title={messages.title} url='/complete-track' doneText={messages.done} + isUpload /> ) } diff --git a/packages/mobile/src/store/drawers/slice.ts b/packages/mobile/src/store/drawers/slice.ts index 314d50b7af2..db3595bd821 100644 --- a/packages/mobile/src/store/drawers/slice.ts +++ b/packages/mobile/src/store/drawers/slice.ts @@ -21,6 +21,7 @@ export type Drawer = | 'UnfavoriteDownloadedCollection' | 'RateCallToAction' | 'LockedContent' + | 'GatedContentUploadPrompt' | 'ChatActions' | 'ProfileActions' | 'BlockMessages' @@ -48,6 +49,7 @@ export type DrawerData = { collectionId: ID } LockedContent: undefined + GatedContentUploadPrompt: undefined ChatActions: { userId: number } ProfileActions: undefined BlockMessages: { userId: number } @@ -74,6 +76,7 @@ const initialState: DrawersState = { UnfavoriteDownloadedCollection: false, RateCallToAction: false, LockedContent: false, + GatedContentUploadPrompt: false, ChatActions: false, ProfileActions: false, BlockMessages: false, diff --git a/packages/web/src/components/data-entry/FormTile.js b/packages/web/src/components/data-entry/FormTile.js index 36c217362fe..d0b283d4b95 100644 --- a/packages/web/src/components/data-entry/FormTile.js +++ b/packages/web/src/components/data-entry/FormTile.js @@ -19,6 +19,7 @@ import Input from 'components/data-entry/Input' import LabeledInput from 'components/data-entry/LabeledInput' import TagInput from 'components/data-entry/TagInput' import TextArea from 'components/data-entry/TextArea' +import { GatedContentUploadPromptModal } from 'components/gated-content-upload-prompt-modal/GatedContentUploadPromptModal' import LabeledButton from 'components/labeled-button/LabeledButton' import Dropdown from 'components/navigation/Dropdown' import ConnectedRemixSettingsModal from 'components/remix-settings-modal/ConnectedRemixSettingsModal' @@ -529,6 +530,15 @@ const AdvancedForm = (props) => { return ( <> + {/* + Render the gated content upload prompt component which is responsible + for whether or its content will modal will be displayed. + */} + {props.type === 'track' && props.isUpload ? ( + setIsAvailabilityModalOpen(true)} + /> + ) : null} {showAvailability && ( void +} + +export const GatedContentUploadPromptModal = ({ + onSubmit +}: GatedContentUploadPromptModalProps) => { + const { isEnabled: isGatedContentEnabled } = useFlag( + FeatureFlags.GATED_CONTENT_ENABLED + ) + const { isEnabled: isSpecialAccessEnabled } = useFlag( + FeatureFlags.SPECIAL_ACCESS_ENABLED + ) + + const [isOpen, setIsOpen] = useState(false) + const { value: seen } = useAsync(() => + localStorage.getItem(GATED_CONTENT_UPLOAD_PROMPT_MODAL_SEEN_KEY) + ) + + useEffect(() => { + const shouldOpen = + isGatedContentEnabled && isSpecialAccessEnabled && seen === null + if (shouldOpen) { + setIsOpen(true) + localStorage.setItem(GATED_CONTENT_UPLOAD_PROMPT_MODAL_SEEN_KEY, 'true') + } + }, [isGatedContentEnabled, isSpecialAccessEnabled, seen, setIsOpen]) + + const handleSubmit = useCallback(() => { + onSubmit() + setIsOpen(false) + }, [onSubmit, setIsOpen]) + + return ( + setIsOpen(false)} + bodyClassName={styles.modalBody} + > + setIsOpen(false)} + className={styles.modalHeader} + dismissButtonClassName={styles.modalHeaderDismissButton} + > + } + /> + + + {messages.subtitle} + {messages.description} + window.open(LEARN_MORE_URL, '_blank')} + iconClassName={styles.learnMoreIcon} + rightIcon={} + /> + + setIsOpen(false)} + /> + } + /> + + + + ) +} diff --git a/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.module.css b/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.module.css index dbc5bb5bd45..6b871f845b0 100644 --- a/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.module.css +++ b/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.module.css @@ -16,7 +16,8 @@ color: var(--neutral-light-2); } -.modalTitleIcon path { +.modalTitleIcon path, +.modalBody .learnMoreArrow path { fill: var(--neutral-light-2); } @@ -33,17 +34,10 @@ font-size: var(--font-l); } -.learnMore { - display: flex; - align-items: center; - width: fit-content; - color: var(--secondary); - font-weight: var(--font-bold); - font-size: var(--font-s); -} - -.learnMore:hover { - color: var(--secondary-dark-2); +.learnMoreButton { + align-self: flex-start; + padding: 0; + color: var(--neutral-light-2); } .learnMoreArrow { diff --git a/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.tsx b/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.tsx index 69347ec078c..b044b221698 100644 --- a/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.tsx +++ b/packages/web/src/components/track-availability-modal/TrackAvailabilityModal.tsx @@ -20,12 +20,12 @@ import { RadioButtonGroup, IconSpecialAccess, IconVisibilityPublic, - IconCollectible, - IconArrow + IconCollectible } from '@audius/stems' import cn from 'classnames' import { useSelector } from 'react-redux' +import { ReactComponent as IconExternalLink } from 'assets/img/iconExternalLink.svg' import { HelpCallout } from 'components/help-callout/HelpCallout' import { ModalRadioItem } from 'components/modal-radio/ModalRadioItem' import { useFlag } from 'hooks/useRemoteConfig' @@ -105,15 +105,14 @@ const CollectibleGatedDescription = ({ {!hasCollectibles && isUpload ? ( ) : null} - - {messages.learnMore} - - + window.open(LEARN_MORE_URL, '_blank')} + iconClassName={styles.learnMoreArrow} + rightIcon={} + /> ) }
{messages.subtitle}
{messages.description}