diff --git a/packages/common/src/store/ui/modals/parentSlice.ts b/packages/common/src/store/ui/modals/parentSlice.ts index 7be766d592a..2bf8ac9daf2 100644 --- a/packages/common/src/store/ui/modals/parentSlice.ts +++ b/packages/common/src/store/ui/modals/parentSlice.ts @@ -9,7 +9,6 @@ import { export const initialState: BasicModalsState = { TiersExplainer: { isOpen: false }, - TrendingRewardsExplainer: { isOpen: false }, ChallengeRewards: { isOpen: false }, ClaimAllRewards: { isOpen: false }, ClaimVestedCoinsModal: { isOpen: false }, @@ -25,6 +24,7 @@ export const initialState: BasicModalsState = { FeedFilter: { isOpen: false }, PurchaseVendor: { isOpen: false }, TrendingGenreSelection: { isOpen: false }, + TrendingRewardsExplainer: { isOpen: false }, SocialProof: { isOpen: false }, EditFolder: { isOpen: false }, EditTrack: { isOpen: false }, diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index d655adad841..8570ec637c9 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -43,7 +43,6 @@ export type CreateChatModalState = { export type Modals = | 'TiersExplainer' - | 'TrendingRewardsExplainer' | 'ChallengeRewards' | 'ClaimAllRewards' | 'ClaimVestedCoinsModal' @@ -59,6 +58,7 @@ export type Modals = | 'FeedFilter' | 'PurchaseVendor' | 'TrendingGenreSelection' + | 'TrendingRewardsExplainer' | 'SocialProof' | 'EditFolder' | 'EditTrack' diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index 503bc912e51..1885b7f1ac9 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -183,29 +183,23 @@ export const challengeRewardsConfig: Partial< progressLabel: 'No Recent Activity', panelButtonText: 'View Details' }, - 'trending-playlist': { - id: 'trending-playlist', - title: 'Trending Playlists Weekly Top 5', + [ChallengeName.TrendingTrack]: { + id: ChallengeName.TrendingTrack, + title: 'Global Trending Weekly Top 5', description: () => 'Top 5 winners are selected every Friday at Noon PT!', panelButtonText: 'See More' }, - tp: { - id: 'trending-playlist', + [ChallengeName.TrendingPlaylist]: { + id: ChallengeName.TrendingPlaylist, title: 'Trending Playlists Weekly Top 5', description: () => 'Top 5 winners are selected every Friday at Noon PT!', panelButtonText: 'See More' }, - 'trending-track': { - title: 'Global Trending Weekly Top 5', - description: () => 'Top 5 winners are selected every Friday at Noon PT!', - panelButtonText: 'See More', - id: 'trending-track' - }, - tt: { - title: 'Global Trending Weekly Top 5', + [ChallengeName.TrendingUndergroundTrack]: { + id: ChallengeName.TrendingUndergroundTrack, + title: 'Underground Trending Weekly Top 5', description: () => 'Top 5 winners are selected every Friday at Noon PT!', - panelButtonText: 'See More', - id: 'trending-track' + panelButtonText: 'See More' }, 'top-api': { title: 'API Apps: Monthly Top 10 ', @@ -220,18 +214,6 @@ export const challengeRewardsConfig: Partial< panelButtonText: 'More Info', id: 'verified-upload' }, - 'trending-underground': { - title: 'Underground Trending Weekly Top 5', - description: () => 'Top 5 winners are selected every Friday at Noon PT!', - panelButtonText: 'See More', - id: 'trending-underground' - }, - tut: { - title: 'Underground Trending Weekly Top 5', - description: () => 'Top 5 winners are selected every Friday at Noon PT!', - panelButtonText: 'See More', - id: 'trending-underground' - }, [ChallengeName.OneShot]: { shortTitle: 'Airdrop 2: Artists', title: 'Airdrop 2: Artist Appreciation', @@ -281,9 +263,9 @@ export const challengeRewardsConfig: Partial< id: ChallengeName.PlayCount1000, title: '1,000 Plays', description: () => - `Hit 1,000 plays across all of your tracks to earn an $AUDIO Reward (requires verification).`, + `Hit 1,000 plays across all of your tracks to earn an $AUDIO Reward`, fullDescription: () => - `Hit 1,000 plays across all of your tracks to earn an $AUDIO Reward (requires verification).`, + `Hit 1,000 plays across all of your tracks to earn an $AUDIO Reward`, progressLabel: '%0 Plays', remainingLabel: '%0 Plays', panelButtonText: 'More Info' @@ -292,9 +274,9 @@ export const challengeRewardsConfig: Partial< id: ChallengeName.PlayCount10000, title: '10,000 Plays', description: () => - `Hit 10,000 plays across all of your tracks to earn an $AUDIO Reward (requires verification).`, + `Hit 10,000 plays across all of your tracks to earn an $AUDIO Reward`, fullDescription: () => - `Hit 10,000 plays across all of your tracks to earn an $AUDIO Reward (requires verification).`, + `Hit 10,000 plays across all of your tracks to earn an $AUDIO Reward`, progressLabel: '%0 Plays', remainingLabel: '%0 Plays', panelButtonText: 'More Info' @@ -364,7 +346,7 @@ export const makeOptimisticChallengeSortComparator = ( return 0 } - // Priority 1: Claimable challenges come first + // Priority 1: Claimable challenges (Ready to Claim) come first if ( userChallenge1.claimableAmount > 0 && userChallenge2.claimableAmount <= 0 @@ -378,7 +360,41 @@ export const makeOptimisticChallengeSortComparator = ( return 1 } - // Priority 2: New and not disbursed challenges come next + // Priority 2: Reward Pending challenges come next + // Reward Pending = completed with cooldown, or has undisbursed specifiers with cooldown + const isRewardPending = (challenge: OptimisticUserChallenge) => { + // Completed with cooldown days + if ( + challenge.state === 'completed' && + challenge.cooldown_days && + challenge.cooldown_days > 0 && + challenge.claimableAmount <= 0 + ) { + return true + } + // Has undisbursed specifiers with cooldown (for Cosign, RemixContestWinner, etc.) + if ( + challenge.undisbursedSpecifiers && + challenge.undisbursedSpecifiers.length > 0 && + challenge.cooldown_days && + challenge.cooldown_days > 0 && + challenge.claimableAmount <= 0 + ) { + return true + } + return false + } + + const isPending1 = isRewardPending(userChallenge1) + const isPending2 = isRewardPending(userChallenge2) + if (isPending1 && !isPending2) { + return -1 + } + if (isPending2 && !isPending1) { + return 1 + } + + // Priority 3: New and not disbursed challenges come next const isNewAndNotDisbursed = (userChallenge: OptimisticUserChallenge) => isNewChallenge(userChallenge.challenge_id) && userChallenge.state !== 'disbursed' @@ -392,7 +408,7 @@ export const makeOptimisticChallengeSortComparator = ( return 1 } - // Priority 3: Non-disbursed come before disbursed + // Priority 4: Non-disbursed come before disbursed if ( userChallenge1.state !== 'disbursed' && userChallenge2.state === 'disbursed' @@ -576,3 +592,23 @@ export const getChallengeStatusLabel = ( return DEFAULT_STATUS_LABELS.AVAILABLE } } + +/** + * Determines if a reward is open to all users or requires verification + * @param rewardId The reward ID to check + * @returns true if the reward is open to all users, false if it requires verification + */ +export const isRewardOpenToAll = (rewardId: ChallengeRewardID): boolean => { + const openToAllRewards = new Set([ + ChallengeName.Tastemaker, + ChallengeName.TrendingPlaylist, + ChallengeName.TrendingTrack, + ChallengeName.TrendingUndergroundTrack, + ChallengeName.AudioMatchingBuy, + ChallengeName.Referred, + ChallengeName.RemixContestWinner, + ChallengeName.Cosign + ]) + + return openToAllRewards.has(rewardId) +} diff --git a/packages/discovery-provider/plugins/pedalboard/apps/anti-abuse-oracle/src/server.tsx b/packages/discovery-provider/plugins/pedalboard/apps/anti-abuse-oracle/src/server.tsx index adcfd90e86b..e7708ed73ce 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/anti-abuse-oracle/src/server.tsx +++ b/packages/discovery-provider/plugins/pedalboard/apps/anti-abuse-oracle/src/server.tsx @@ -40,7 +40,32 @@ if (!AAO_AUTH_PASSWORD) { } const rewardAmountRatio = 10 -const skipValidationChallenges = ['dvl'] +const openRewards = [ + 'dvl', // daily volume rewards + 't', // tastemaker + 'tp', // trending playlists + 'tt', // trending + 'tut', // trending underground + 'b', // audio match buy (from verified user) + 'rd', // referred (by verified user) + 'w', // remix contest winner (from verified user) + 'cs', // cosign (from verified user) +] + +const verifiedRewards = [ + 'u', // uploads + 's', // audio match sell + 'r', // referral + 'c', // first comment + 'cp', // comment pin + 'e', // listen streak + 'fp', // first playlist + 'm', // mobile install + 'p', // profile completion + 'p1', // 250 plays + 'p2', // 1000 plays + 'p3', // 10000 plays +] const sdk = getAudiusSdk() @@ -174,27 +199,21 @@ app.post('/attestation/:handle', async (c) => { return c.json({ error: `handle not found: ${handle}` }, 404) } const user = users[0]! - - if (!skipValidationChallenges.includes(challengeId)) { - // pass / fail + if (verifiedRewards.includes(challengeId)) { + if (!user.isVerified) { + return c.json({ error: 'denied' }, 400) + } + } + if (openRewards.includes(challengeId)) { const userScore = await getUserNormalizedScore( HashId.parse(user.id), user.wallet ) - - // Reward attestation proportional to user score confidence - if (userScore.overallScore < (amount as number) / rewardAmountRatio) { + if (userScore.overallScore < -1000) { return c.json({ error: 'denied' }, 400) } - - // Custom rules for specific challenges - if (challengeId === 'e') { - if (!user.isVerified) { - return c.json({ error: 'denied' }, 400) - } - } - console.log('userScore', userScore, user) } + try { const bnAmount = SolanaUtils.uiAudioToBNWaudio(amount) const identifier = SolanaUtils.constructTransferId( diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index 95ea830e0fb..ec2f1357c52 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -44,7 +44,6 @@ import { SignOutConfirmationDrawer } from 'app/components/sign-out-confirmation- import { StripeOnrampDrawer } from 'app/components/stripe-onramp-drawer' import { SupportersInfoDrawer } from 'app/components/supporters-info-drawer' import { TransferAudioMobileDrawer } from 'app/components/transfer-audio-mobile-drawer' -import { TrendingRewardsDrawer } from 'app/components/trending-rewards-drawer' import { VerificationErrorDrawer } from 'app/components/verification-error-drawer/VerificationErrorDrawer' import { VerificationSuccessDrawer } from 'app/components/verification-success-drawer/VerificationSuccessDrawer' import { WaitForDownloadDrawer } from 'app/components/wait-for-download-drawer' @@ -107,7 +106,6 @@ export const NativeDrawer = (props: NativeDrawerProps) => { const commonDrawersMap: { [Modal in Modals]?: ComponentType } = { TiersExplainer: TiersExplainerDrawer, - TrendingRewardsExplainer: TrendingRewardsDrawer, ChallengeRewards: ChallengeRewardsDrawer, ClaimAllRewards: ClaimAllRewardsDrawer, APIRewardsExplainer: ApiRewardsDrawer, diff --git a/packages/mobile/src/components/audio-rewards/RewardsBanner.tsx b/packages/mobile/src/components/audio-rewards/RewardsBanner.tsx deleted file mode 100644 index 73e0c46d7c2..00000000000 --- a/packages/mobile/src/components/audio-rewards/RewardsBanner.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useCallback } from 'react' - -import { audioRewardsPageActions, modalsActions } from '@audius/common/store' -import LinearGradient from 'react-native-linear-gradient' -import { useDispatch } from 'react-redux' - -import { IconCrown, Flex, Text, Paper, useTheme } from '@audius/harmony-native' -const { setVisibility } = modalsActions -const { setTrendingRewardsModalType } = audioRewardsPageActions - -const messageMap = { - tracks: { - title: 'Global Trending: Weekly Top 5', - description: 'Artists trending Fridays at 12PM PT win tokens!' - }, - playlists: { - title: 'Trending Playlists: Weekly Top 5', - description: 'Playlists trending Fridays at 12PM PT win tokens!' - }, - underground: { - title: 'Underground Trending: Weekly Top 5', - description: 'Artists trending Fridays at 12PM PT win tokens!' - } -} - -type RewardsBannerProps = { - bannerType?: 'tracks' | 'playlists' | 'underground' -} - -export const RewardsBanner = (props: RewardsBannerProps) => { - const { bannerType = 'tracks' } = props - const dispatch = useDispatch() - const { color } = useTheme() - - const handlePress = useCallback(() => { - dispatch(setTrendingRewardsModalType({ modalType: bannerType })) - dispatch( - setVisibility({ modal: 'TrendingRewardsExplainer', visible: true }) - ) - }, [dispatch, bannerType]) - - // Get message content safely - const messageContent = messageMap[bannerType] || messageMap.tracks - - return ( - - - - - - - {messageContent.title} - - - - {messageContent.description} - - - - - ) -} diff --git a/packages/mobile/src/components/audio-rewards/index.ts b/packages/mobile/src/components/audio-rewards/index.ts index ab5fcbfc160..c4cb89e566f 100644 --- a/packages/mobile/src/components/audio-rewards/index.ts +++ b/packages/mobile/src/components/audio-rewards/index.ts @@ -1,4 +1,3 @@ export * from '../core/IconAudioBadge' export * from './TiersExplainerDrawer' -export * from './RewardsBanner' export * from './TierText' diff --git a/packages/mobile/src/components/drawer/DrawerHeader.tsx b/packages/mobile/src/components/drawer/DrawerHeader.tsx index 48c7e1e865f..50c1f83f02a 100644 --- a/packages/mobile/src/components/drawer/DrawerHeader.tsx +++ b/packages/mobile/src/components/drawer/DrawerHeader.tsx @@ -23,7 +23,7 @@ export const useStyles = makeStyles(({ spacing }) => ({ alignItems: 'center', justifyContent: 'center', paddingVertical: spacing(2), - paddingHorizontal: spacing(8), + paddingHorizontal: spacing(10), zIndex: zIndex.DRAWER_HEADER }, diff --git a/packages/mobile/src/components/progress-bar/ProgressBar.tsx b/packages/mobile/src/components/progress-bar/ProgressBar.tsx index 02e565caf12..17a5c995dcb 100644 --- a/packages/mobile/src/components/progress-bar/ProgressBar.tsx +++ b/packages/mobile/src/components/progress-bar/ProgressBar.tsx @@ -2,6 +2,7 @@ import type { ViewStyle } from 'react-native' import { View } from 'react-native' import LinearGradient from 'react-native-linear-gradient' +import { useTheme } from '@audius/harmony-native' import type { StylesProp } from 'app/styles' import { makeStyles } from 'app/styles' import { useThemeColors } from 'app/utils/theme' @@ -55,28 +56,50 @@ const useStyles = makeStyles(({ spacing, palette }) => ({ type ProgressBarProps = { progress: number max: number + variant?: 'default' | 'coin' style?: StylesProp<{ root: ViewStyle }> } -export const ProgressBar = ({ progress, max, style }: ProgressBarProps) => { +export const ProgressBar = ({ + progress, + max, + variant = 'default', + style +}: ProgressBarProps) => { const styles = useStyles() const { pageHeaderGradientColor1, pageHeaderGradientColor2 } = useThemeColors() + const { color, spacing } = useTheme() + + const progressWidth = + progress > max + ? '100%' + : `${Math.round((progress * 100 * 100.0) / max) / 100.0}%` + + const gradientProps = + variant === 'coin' + ? { + ...color.special.coinGradient, + start: { x: 0, y: 0.5 }, + end: { x: 1, y: 0.5 } + } + : { + colors: [pageHeaderGradientColor1, pageHeaderGradientColor2], + useAngle: true, + angle: 315 + } + return ( max - ? '100%' - : `${Math.round((progress * 100 * 100.0) / max) / 100.0}%` + width: progressWidth as any, + ...(variant === 'coin' && { borderRadius: spacing['3xl'] }) } ]} /> diff --git a/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen.tsx b/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen.tsx index 94a354ec22a..d6c666c3ca8 100644 --- a/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen.tsx +++ b/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingPlaylistsScreen.tsx @@ -5,7 +5,6 @@ import { } from '@audius/common/store' import { useSelector } from 'react-redux' -import { RewardsBanner } from 'app/components/audio-rewards' import { Screen, ScreenContent, ScreenHeader } from 'app/components/core' import { Lineup } from 'app/components/lineup' const { getLineup } = trendingPlaylistsPageLineupSelectors @@ -26,7 +25,6 @@ export const TrendingPlaylistsScreen = () => { } actions={trendingPlaylistsPageLineupActions} rankIconCount={5} isTrending diff --git a/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingUndergroundScreen.tsx b/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingUndergroundScreen.tsx index 5a244cbf12e..ebca746c107 100644 --- a/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingUndergroundScreen.tsx +++ b/packages/mobile/src/screens/explore-screen/tabs/ForYouTab/TrendingUndergroundScreen.tsx @@ -5,7 +5,6 @@ import { } from '@audius/common/store' import { useSelector } from 'react-redux' -import { RewardsBanner } from 'app/components/audio-rewards' import { Screen, ScreenContent, ScreenHeader } from 'app/components/core' import { Lineup } from 'app/components/lineup' const { makeGetLineupMetadatas } = lineupSelectors @@ -26,7 +25,6 @@ export const TrendingUndergroundScreen = () => { } actions={trendingUndergroundPageLineupActions} rankIconCount={5} isTrending diff --git a/packages/mobile/src/screens/rewards-screen/ChallengeRewardsTile.tsx b/packages/mobile/src/screens/rewards-screen/ChallengeRewardsTile.tsx index 36c4013957a..6d94c0a3eb6 100644 --- a/packages/mobile/src/screens/rewards-screen/ChallengeRewardsTile.tsx +++ b/packages/mobile/src/screens/rewards-screen/ChallengeRewardsTile.tsx @@ -1,15 +1,14 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useCurrentAccount, useCurrentAccountUser } from '@audius/common/api' -import { useRemoteVar } from '@audius/common/hooks' import { Name, ChallengeName } from '@audius/common/models' import type { ChallengeRewardID } from '@audius/common/models' -import { StringKeys } from '@audius/common/services' import { challengesSelectors, audioRewardsPageSelectors, audioRewardsPageActions, - modalsActions + modalsActions, + useTierAndVerifiedForUser } from '@audius/common/store' import type { ChallengeRewardsModalType, @@ -17,19 +16,35 @@ import type { } from '@audius/common/store' import type { dayjs } from '@audius/common/utils' import { + isRewardOpenToAll, removeNullable, makeOptimisticChallengeSortComparator } from '@audius/common/utils' +import { BlurView } from '@react-native-community/blur' import { useFocusEffect } from '@react-navigation/native' +import { View } from 'react-native' import { useDispatch, useSelector } from 'react-redux' -import { Flex, Text, Paper } from '@audius/harmony-native' +import { + Flex, + IconCaretRight, + IconLock, + IconVerified, + PlainButton, + SelectablePill, + Text, + Paper, + useTheme +} from '@audius/harmony-native' import { GradientText } from 'app/components/core' import LoadingSpinner from 'app/components/loading-spinner' import type { SummaryTableItem } from 'app/components/summary-table/SummaryTable' +import { useNavigation } from 'app/hooks/useNavigation' +import type { ProfileTabScreenParamList } from 'app/screens/app-screen/ProfileTabScreen' import { make, track } from 'app/services/analytics' import { makeStyles } from 'app/styles' import { getChallengeConfig } from 'app/utils/challenges' +import { Theme, useThemeVariant } from 'app/utils/theme' import { Panel } from './Panel' const { setVisibility } = modalsActions @@ -39,47 +54,24 @@ const { fetchUserChallenges, setChallengeRewardsModalType } = audioRewardsPageActions const { getOptimisticUserChallenges } = challengesSelectors -const validRewardIds: Set = new Set([ - 'track-upload', - 'mobile-install', - 'listen-streak', - 'profile-completion', - 'first-playlist', - ChallengeName.AudioMatchingBuy, // $AUDIO matching buyer - ChallengeName.AudioMatchingSell, // $AUDIO matching seller - ChallengeName.FirstPlaylist, - ChallengeName.ListenStreak, - ChallengeName.ListenStreakEndless, - ChallengeName.MobileInstall, - ChallengeName.ProfileCompletion, - ChallengeName.Referrals, - ChallengeName.ReferralsVerified, - ChallengeName.Referred, - ChallengeName.TrackUpload, - ChallengeName.OneShot, - ChallengeName.FirstWeeklyComment, - ChallengeName.PlayCount250, - ChallengeName.PlayCount1000, - ChallengeName.PlayCount10000, - ChallengeName.Tastemaker, - ChallengeName.Cosign, - ChallengeName.CommentPin, - ChallengeName.RemixContestWinner -]) - type ClaimableSummaryTableItem = SummaryTableItem & { claimableDate: dayjs.Dayjs isClose: boolean } const messages = { - title: 'Achievement Rewards', + title: 'Rewards', subheader: 'Earn $AUDIO by completing simple tasks while using Audius.', pending: 'Pending', claimAllRewards: 'Claim All Rewards', moreInfo: 'More Info', available: '$AUDIO available', now: 'now!', + showCompleted: 'Show Completed', + hideCompleted: 'Hide Completed', + required: 'Required', + getVerifiedMessage: 'Get verified for access to even more rewards!', + settings: 'Settings', availableMessage: (summaryItems: ClaimableSummaryTableItem[]) => { const filteredSummaryItems = summaryItems.filter(removeNullable) const summaryItem = filteredSummaryItems.pop() @@ -96,18 +88,6 @@ const messages = { ) } } -/** Pulls rewards from remoteconfig */ -const useRewardIds = ( - hideConfig: Partial> -) => { - const rewardsString = useRemoteVar(StringKeys.CHALLENGE_REWARD_IDS) - if (rewardsString === null) return [] - const rewards = rewardsString.split(',') as ChallengeRewardID[] - const filteredRewards: ChallengeRewardID[] = rewards.filter( - (reward) => validRewardIds.has(reward) && !hideConfig[reward] - ) - return filteredRewards -} const useStyles = makeStyles(({ spacing, typography, palette }) => ({ title: { @@ -123,6 +103,11 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({ export const ChallengeRewardsTile = () => { const styles = useStyles() const dispatch = useDispatch() + const navigation = useNavigation() + const { spacing } = useTheme() + const themeVariant = useThemeVariant() + const isDarkMode = + themeVariant === Theme.DARK || themeVariant === Theme.MATRIX const userChallengesLoading = useSelector(getUserChallengesLoading) const userChallenges = useSelector(getUserChallenges) const { data: currentAccount } = useCurrentAccount() @@ -131,12 +116,8 @@ export const ChallengeRewardsTile = () => { getOptimisticUserChallenges(state, currentAccount, currentUser) ) const [haveChallengesLoaded, setHaveChallengesLoaded] = useState(false) - - // The referred challenge only needs a tile if the user was referred - const hideReferredTile = !userChallenges[ChallengeName.Referred]?.is_complete - const rewardIds = useRewardIds({ - [ChallengeName.Referred]: hideReferredTile - }) + const [showCompleted, setShowCompleted] = useState(false) + const { isVerified } = useTierAndVerifiedForUser(currentUser?.user_id) useEffect(() => { if (!userChallengesLoading && !haveChallengesLoaded) { @@ -155,43 +136,285 @@ export const ChallengeRewardsTile = () => { dispatch(setVisibility({ modal: 'ChallengeRewards', visible: true })) } - const rewardsPanels = rewardIds - // Filter out challenges that DN didn't return - .map((id) => userChallenges[id]?.challenge_id) - .filter(removeNullable) - .sort(makeOptimisticChallengeSortComparator(optimisticUserChallenges)) - .map((id) => { - const props = getChallengeConfig(id) - const onPress = () => { - openModal(id) - track( - make({ - eventName: Name.REWARDS_CLAIM_DETAILS_OPENED, - challengeId: id - }) - ) + // Get all reward IDs from API (like web does) + const rewardIdsSorted = useMemo(() => { + const allRewardIds = Object.keys(userChallenges).filter((id) => { + const challengeId = id as ChallengeRewardID + // The referred challenge only needs a tile if the user was referred + if (challengeId === ChallengeName.Referred) { + return userChallenges[challengeId]?.is_complete === true } - return ( - - ) + // Include all other challenges + return true + }) as ChallengeRewardID[] + + return allRewardIds.sort( + makeOptimisticChallengeSortComparator(optimisticUserChallenges) + ) + }, [optimisticUserChallenges, userChallenges]) + + // Filter completed rewards based on toggle + const filteredRewardIds = useMemo(() => { + if (showCompleted) { + return rewardIdsSorted + } + return rewardIdsSorted.filter((id) => { + const challenge = optimisticUserChallenges[id] + if (!challenge) return true + const hasDisbursed = + challenge.state === 'disbursed' || + (challenge.challenge_id === ChallengeName.OneShot && + challenge.disbursed_amount > 0) + return !hasDisbursed }) + }, [rewardIdsSorted, optimisticUserChallenges, showCompleted]) + + // When verified, combine all rewards and sort by claimability + // When not verified, separate into open-to-all and verified-only + const { allRewardsSorted, openToAllRewards, verifiedOnlyRewards } = + useMemo(() => { + if (isVerified) { + // When verified, combine all rewards and sort by claimability + const allRewards = [...filteredRewardIds].sort( + makeOptimisticChallengeSortComparator(optimisticUserChallenges) + ) + return { + allRewardsSorted: allRewards, + openToAllRewards: [], + verifiedOnlyRewards: [] + } + } else { + // When not verified, separate into open-to-all and verified-only + const openToAll: typeof filteredRewardIds = [] + const verifiedOnly: typeof filteredRewardIds = [] + + filteredRewardIds.forEach((id) => { + if (isRewardOpenToAll(id)) { + openToAll.push(id) + } else { + verifiedOnly.push(id) + } + }) + + return { + allRewardsSorted: [], + openToAllRewards: openToAll, + verifiedOnlyRewards: verifiedOnly + } + } + }, [filteredRewardIds, isVerified, optimisticUserChallenges]) + + const hasLockedRewards = !isVerified && verifiedOnlyRewards.length > 0 + + // Create panels for each reward type + const allRewardsPanels = isVerified + ? allRewardsSorted.map((id) => { + const props = getChallengeConfig(id) + const onPress = () => { + openModal(id) + track( + make({ + eventName: Name.REWARDS_CLAIM_DETAILS_OPENED, + challengeId: id + }) + ) + } + return ( + + ) + }) + : [] + + const openToAllPanels = !isVerified + ? openToAllRewards.map((id) => { + const props = getChallengeConfig(id) + const onPress = () => { + openModal(id) + track( + make({ + eventName: Name.REWARDS_CLAIM_DETAILS_OPENED, + challengeId: id + }) + ) + } + return ( + + ) + }) + : [] + + const verifiedOnlyPanels = !isVerified + ? verifiedOnlyRewards.map((id) => { + const props = getChallengeConfig(id) + const onPress = () => { + openModal(id) + track( + make({ + eventName: Name.REWARDS_CLAIM_DETAILS_OPENED, + challengeId: id + }) + ) + } + return ( + + ) + }) + : [] return ( - - {messages.title} - {messages.subheader} - + + + {messages.title} + {messages.subheader} + + + setShowCompleted(!showCompleted)} + /> + + {userChallengesLoading && !haveChallengesLoaded ? ( ) : ( - {rewardsPanels} + <> + {isVerified && allRewardsPanels.length > 0 && ( + {allRewardsPanels} + )} + {!isVerified && openToAllPanels.length > 0 && ( + {openToAllPanels} + )} + {hasLockedRewards && ( + + {verifiedOnlyPanels} + + + + + + {messages.required} + + + + + + + + {messages.getVerifiedMessage} + + { + navigation.push('AccountSettingsScreen') + }} + iconRight={IconCaretRight} + > + {messages.settings} + + + + + )} + {!hasLockedRewards && verifiedOnlyPanels.length > 0 && ( + {verifiedOnlyPanels} + )} + )} diff --git a/packages/mobile/src/screens/rewards-screen/ClaimAllRewardsTile.tsx b/packages/mobile/src/screens/rewards-screen/ClaimAllRewardsTile.tsx index 00c7899c810..bc529e78f35 100644 --- a/packages/mobile/src/screens/rewards-screen/ClaimAllRewardsTile.tsx +++ b/packages/mobile/src/screens/rewards-screen/ClaimAllRewardsTile.tsx @@ -1,27 +1,32 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' -import { - formatCooldownChallenges, - useChallengeCooldownSchedule -} from '@audius/common/hooks' -import { modalsActions } from '@audius/common/store' -import { Image, View } from 'react-native' -import { useDispatch } from 'react-redux' +import { useCurrentAccount, useCurrentAccountUser } from '@audius/common/api' +import { useChallengeCooldownSchedule } from '@audius/common/hooks' +import type { CommonState } from '@audius/common/store' +import { challengesSelectors, modalsActions } from '@audius/common/store' +import { formatNumberCommas } from '@audius/common/utils' +import { useDispatch, useSelector } from 'react-redux' import { Button, + Divider, Flex, - Text, + IconArrowRight, Paper, - IconArrowRight + Text } from '@audius/harmony-native' -import TokenStill from 'app/assets/images/tokenSpinStill.png' -import { makeStyles } from 'app/styles' +import { TooltipInfoIcon } from 'app/components/buy-sell/TooltipInfoIcon' + +const { getOptimisticUserChallenges } = challengesSelectors const { setVisibility } = modalsActions const messages = { - pending: 'Pending', + yourRewards: 'Your Rewards', + totalClaimed: 'TOTAL CLAIMED', + pending: 'PENDING', + readyToClaim: 'READY TO CLAIM', + claimAll: 'Claim All', claimAllRewards: 'Claim All Rewards', moreInfo: 'More Info', available: '$AUDIO available', @@ -43,39 +48,26 @@ const messages = { } } -const useStyles = makeStyles(({ spacing, typography }) => ({ - pillContainer: { - height: spacing(6), - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'flex-start' - }, - pillMessage: { - paddingVertical: spacing(1), - paddingHorizontal: spacing(2), - fontSize: typography.fontSize.small, - fontFamily: typography.fontByWeight.demiBold, - lineHeight: spacing(4), - borderWidth: 1, - borderRadius: 12, - borderColor: 'rgba(133,129,153,0.1)', - overflow: 'hidden' - }, - readyToClaimPill: { - backgroundColor: 'rgba(133,129,153,0.1)' - }, - token: { - width: 24, - height: 24 - } -})) - export const ClaimAllRewardsTile = () => { - const styles = useStyles() const dispatch = useDispatch() - const { cooldownChallenges, cooldownAmount, claimableAmount, isEmpty } = + const { cooldownAmount, claimableAmount, isEmpty } = useChallengeCooldownSchedule({ multiple: true }) + const { data: currentAccount } = useCurrentAccount() + const { data: currentUser } = useCurrentAccountUser() + const optimisticUserChallenges = useSelector((state: CommonState) => + getOptimisticUserChallenges(state, currentAccount, currentUser) + ) + + // Calculate total claimed amount + const totalClaimed = useMemo(() => { + return Object.values(optimisticUserChallenges).reduce( + (sum, challenge) => sum + (challenge?.disbursed_amount ?? 0), + 0 + ) + }, [optimisticUserChallenges]) + + // Pending amount is the cooldown amount + const pendingAmount = cooldownAmount const openClaimAllModal = useCallback(() => { dispatch(setVisibility({ modal: 'ClaimAllRewards', visible: true })) @@ -83,50 +75,90 @@ export const ClaimAllRewardsTile = () => { if (isEmpty) return null + const tooltipMessages = { + totalClaimed: 'Total amount of $AUDIO you have claimed from all rewards', + pending: 'Amount of $AUDIO pending in cooldown period', + readyToClaim: 'Amount of $AUDIO ready to claim now' + } + return ( - - - {claimableAmount > 0 ? ( - - ) : null} - - {messages.totalReadyToClaim} - - - {cooldownAmount > 0 ? ( - - - {cooldownAmount} {messages.pending} - - - ) : null} - - {claimableAmount > 0 - ? `${claimableAmount} ${messages.available} ${messages.now}` - : messages.availableMessage( - formatCooldownChallenges(cooldownChallenges) - )} + + + {messages.yourRewards} - - {claimableAmount > 0 ? ( - - ) : cooldownAmount > 0 ? ( - - ) : null} + + + + + + {formatNumberCommas(totalClaimed)} + + + $AUDIO + + + + + {messages.totalClaimed} + + + + + + + + + {formatNumberCommas(pendingAmount)} + + + $AUDIO + + + + + {messages.pending} + + + + + + {/* Second row: Ready to Claim */} + + + + {formatNumberCommas(claimableAmount)} + + + $AUDIO + + + + + {messages.readyToClaim} + + + + + {claimableAmount > 0 ? ( + + ) : null} ) } diff --git a/packages/mobile/src/screens/rewards-screen/Panel.tsx b/packages/mobile/src/screens/rewards-screen/Panel.tsx index b572a8d1471..c6d378cac71 100644 --- a/packages/mobile/src/screens/rewards-screen/Panel.tsx +++ b/packages/mobile/src/screens/rewards-screen/Panel.tsx @@ -1,18 +1,14 @@ +import { useMemo } from 'react' + import { useFormattedProgressLabel } from '@audius/common/hooks' import type { OptimisticUserChallenge } from '@audius/common/models' import type { ChallengeRewardsInfo } from '@audius/common/utils' -import { isNewChallenge } from '@audius/common/utils' +import { getChallengeStatusLabel } from '@audius/common/utils' import { Platform } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' -import { - Flex, - IconCheck, - IconSparkles, - Text, - useTheme -} from '@audius/harmony-native' -import { ProgressBar } from 'app/components/progress-bar' +import { Flex, IconCheck, Text, useTheme } from '@audius/harmony-native' +import { ProgressBar } from 'app/components/progress-bar/ProgressBar' import type { MobileChallengeConfig } from 'app/utils/challenges' import { useThemeColors } from 'app/utils/theme' @@ -21,8 +17,7 @@ const messages = { claimReward: 'Claim This Reward', readyToClaim: 'Ready to Claim', pendingRewards: 'Reward Pending', - viewDetails: 'View Details', - new: 'New!' + viewDetails: 'View Details' } type PanelProps = { @@ -43,7 +38,7 @@ export const Panel = ({ challenge }: PanelProps) => { const { neutralLight4 } = useThemeColors() - const { spacing } = useTheme() + const { spacing, color } = useTheme() const maxStepCount = challenge?.max_steps ?? 0 const hasDisbursed = challenge?.state === 'disbursed' @@ -52,19 +47,53 @@ export const Panel = ({ challenge?.challenge_type !== 'aggregate' && !hasDisbursed const needsDisbursement = challenge && challenge.claimableAmount > 0 - const showNewChallengePill = isNewChallenge(id) - - const shouldShowProgressLabel = !!progressLabel const formattedProgressLabel: string = useFormattedProgressLabel({ challenge, progressLabel, remainingLabel }) + + // Determine the final label to display + // If there's no progress bar, no "Ready to Claim" status, and the formatted label is empty or not meaningful, + // show "Available" instead + const displayLabel = useMemo(() => { + // If there's a progress bar or "Ready to Claim" status, use the formatted label + if (shouldShowProgressBar || needsDisbursement) { + return formattedProgressLabel + } + + // If the formatted label is empty or just whitespace, use getChallengeStatusLabel + // which will return "Available" for challenges without meaningful status + if (!formattedProgressLabel || formattedProgressLabel.trim() === '') { + return getChallengeStatusLabel(challenge, id) + } + + // Otherwise use the formatted label + return formattedProgressLabel + }, [ + formattedProgressLabel, + shouldShowProgressBar, + needsDisbursement, + challenge, + id + ]) + return ( - - + + {needsDisbursement ? ( - ) : showNewChallengePill ? ( - - - - {messages.new} - - ) : null} @@ -102,30 +117,36 @@ export const Panel = ({ {shortDescription || description(challenge)} - {shouldShowProgressLabel ? ( - - {hasDisbursed ? ( - - ) : null} - - - {formattedProgressLabel} - - + + {hasDisbursed ? ( + + ) : null} + + + {displayLabel} + - ) : null} + {shouldShowProgressBar ? ( ) : null} diff --git a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx index fa6d01ef6f6..9b8140b9f77 100644 --- a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx +++ b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx @@ -17,7 +17,6 @@ import { useThemeColors } from 'app/utils/theme' import { ChallengeRewardsTile } from './ChallengeRewardsTile' import { ClaimAllRewardsTile } from './ClaimAllRewardsTile' import { TiersTile } from './TiersTile' -import { TrendingRewardsTile } from './TrendingRewardsTile' const { fetchAssociatedWallets } = tokenDashboardPageActions @@ -122,7 +121,6 @@ export const RewardsScreen = () => { {audioFeaturesDegradedText ? renderNoticeTile() : null} - diff --git a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx index 37d4e7031c0..ebca2560060 100644 --- a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx +++ b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx @@ -42,7 +42,7 @@ const audioTierMapSvg: { const LEARN_MORE_LINK = 'https://blog.audius.co/article/community-meet-audio' const messages = { - vipTiers: 'Reward Perks', + vipTiers: 'Perks', vipTiersBody: 'Keep $AUDIO in your wallet to enjoy perks and exclusive features.', launchDiscord: 'Launch the VIP Discord', diff --git a/packages/mobile/src/screens/trending-screen/TrendingScreen.tsx b/packages/mobile/src/screens/trending-screen/TrendingScreen.tsx index ed966984ee0..def624de7c7 100644 --- a/packages/mobile/src/screens/trending-screen/TrendingScreen.tsx +++ b/packages/mobile/src/screens/trending-screen/TrendingScreen.tsx @@ -1,6 +1,4 @@ import { TimeRange } from '@audius/common/models' -import { trendingPageSelectors } from '@audius/common/store' -import { useSelector } from 'react-redux' import { IconAllTime, @@ -8,7 +6,6 @@ import { IconCalendarMonth, IconTrending } from '@audius/harmony-native' -import { RewardsBanner } from 'app/components/audio-rewards' import { Screen, ScreenContent, ScreenHeader } from 'app/components/core' import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent' import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent' @@ -17,17 +14,9 @@ import { useAppTabScreen } from 'app/hooks/useAppTabScreen' import { TrendingFilterButton } from './TrendingFilterButton' import { TrendingLineup } from './TrendingLineup' -const { getTrendingGenre } = trendingPageSelectors const ThisWeekTab = () => { - const trendingGenre = useSelector(getTrendingGenre) - return ( - } - timeRange={TimeRange.WEEK} - rankIconCount={5} - /> - ) + return } const ThisMonthTab = () => { return diff --git a/packages/web/src/components/coin-progress-bar/CoinProgressBar.tsx b/packages/web/src/components/coin-progress-bar/CoinProgressBar.tsx new file mode 100644 index 00000000000..20d1f063d3e --- /dev/null +++ b/packages/web/src/components/coin-progress-bar/CoinProgressBar.tsx @@ -0,0 +1,39 @@ +import { Box, useTheme } from '@audius/harmony' + +type CoinProgressBarProps = { + progress: number + max: number +} + +export const CoinProgressBar = ({ progress, max }: CoinProgressBarProps) => { + const { color } = useTheme() + + const percentage = Math.min(100, Math.max(0, (progress / max) * 100)) + + return ( + + + + ) +} diff --git a/packages/web/src/components/rewards/modals/TrendingRewardsModal.tsx b/packages/web/src/components/rewards/modals/TrendingRewardsModal.tsx deleted file mode 100644 index 613058cd47a..00000000000 --- a/packages/web/src/components/rewards/modals/TrendingRewardsModal.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' - -import { Theme } from '@audius/common/models' -import { StringKeys } from '@audius/common/services' -import { - audioRewardsPageSelectors, - audioRewardsPageActions, - TrendingRewardsModalType -} from '@audius/common/store' -import { route } from '@audius/common/utils' -import { - SegmentedControl, - IconArrowRight as IconArrow, - Button, - IconTrending, - Text, - Paper, - LoadingSpinner, - Box, - Flex -} from '@audius/harmony' -import { useDispatch } from 'react-redux' -import { TwitterTweetEmbed } from 'react-twitter-embed' - -import { useModalState } from 'common/hooks/useModalState' -import { TextLink } from 'components/link' -import ModalDrawer from 'components/modal-drawer/ModalDrawer' -import { useIsMobile } from 'hooks/useIsMobile' -import { useNavigateToPage } from 'hooks/useNavigateToPage' -import { useRemoteVar } from 'hooks/useRemoteConfig' -import { useSelector } from 'utils/reducer' -import { getTheme, isDarkMode } from 'utils/theme/theme' - -const { TRENDING_PAGE, TRENDING_PLAYLISTS_PAGE, TRENDING_UNDERGROUND_PAGE } = - route -const { getTrendingRewardsModalType } = audioRewardsPageSelectors -const { setTrendingRewardsModalType } = audioRewardsPageActions - -const messages = { - tracksTitle: 'Top 5 Tracks Each Week Receive 1000 $AUDIO', - playlistTitle: 'Top 5 Playlists Each Week Receive 100 $AUDIO', - undergroundTitle: 'Top 5 Tracks Each Week Receive 1000 $AUDIO', - winners: 'Winners are selected every Friday at Noon PT!', - lastWeek: "LAST WEEK'S WINNERS", - tracks: 'Tracks', - topTracks: 'Top Tracks', - playlists: 'Playlists', - topPlaylists: 'Top Playlists', - underground: 'Underground', - terms: 'Terms and Conditions Apply', - tracksModalTitle: 'Top 5 Trending Tracks', - playlistsModalTitle: 'Top 5 Trending Playlists', - undergroundModalTitle: 'Top 5 Underground Trending Tracks', - buttonTextTracks: 'Current Trending Tracks', - buttonTextPlaylists: 'Current Trending Playlists', - buttonTextUnderground: 'Current Underground Trending Tracks', - mobileButtonTextTracks: 'Trending Tracks', - mobileButtonTextPlaylists: 'Trending Playlists', - mobileButtonTextUnderground: 'Underground Trending Tracks', - arrowCurveUp: 'arrow-curve-up', - chartBar: 'chart-bar', - chartIncreasing: 'chart-increasing' -} - -const TRENDING_PAGES = { - tracks: TRENDING_PAGE, - playlists: TRENDING_PLAYLISTS_PAGE, - underground: TRENDING_UNDERGROUND_PAGE -} - -const textMap = { - playlists: { - modalTitle: messages.playlistsModalTitle, - title: messages.playlistTitle, - button: messages.buttonTextPlaylists, - buttonMobile: messages.mobileButtonTextPlaylists, - icon: messages.arrowCurveUp - }, - tracks: { - modalTitle: messages.tracksModalTitle, - title: messages.tracksTitle, - button: messages.buttonTextTracks, - buttonMobile: messages.mobileButtonTextTracks, - icon: messages.chartIncreasing - }, - underground: { - modalTitle: messages.undergroundModalTitle, - title: messages.undergroundTitle, - button: messages.buttonTextUnderground, - buttonMobile: messages.mobileButtonTextUnderground, - icon: messages.chartBar - } -} - -const TOS_URL = 'https://blog.audius.co/posts/audio-rewards' - -// Getters and setters for whether we're looking at -// trending playlists or trending tracks -const useRewardsType = (): [ - TrendingRewardsModalType, - (type: TrendingRewardsModalType) => void -] => { - const dispatch = useDispatch() - const rewardsType = useSelector(getTrendingRewardsModalType) - const setTrendingRewardsType = useCallback( - (type: TrendingRewardsModalType) => { - dispatch(setTrendingRewardsModalType({ modalType: type })) - }, - [dispatch] - ) - return [rewardsType, setTrendingRewardsType] -} - -const useTweetId = (type: TrendingRewardsModalType) => { - const tracksId = useRemoteVar(StringKeys.REWARDS_TWEET_ID_TRACKS) - const playlistsId = useRemoteVar(StringKeys.REWARDS_TWEET_ID_PLAYLISTS) - const undergroundId = useRemoteVar(StringKeys.REWARDS_TWEET_ID_UNDERGROUND) - return { - tracks: tracksId, - playlists: playlistsId, - underground: undergroundId - }[type] -} - -const shouldUseDarkTwitter = () => { - const theme = getTheme() - return theme === Theme.MATRIX || isDarkMode() -} - -const TrendingRewardsModal = () => { - const [isOpen, setOpen] = useModalState('TrendingRewardsExplainer') - const [modalType, setModalType] = useRewardsType() - - const isMobile = useIsMobile() - const tabOptions = [ - { - key: 'tracks', - text: isMobile ? messages.tracks : messages.topTracks - }, - { - key: 'playlists', - text: isMobile ? messages.playlists : messages.topPlaylists - }, - { - key: 'underground', - text: messages.underground - } - ] - - const navigate = useNavigateToPage() - - const onButtonClick = useCallback(() => { - const page = TRENDING_PAGES[modalType] - navigate(page) - setOpen(false) - }, [navigate, modalType, setOpen]) - - // If we change type, show the spinner again - const [showSpinner, setShowSpinner] = useState(true) - useEffect(() => { - setShowSpinner(true) - }, [modalType]) - - const tweetId = useTweetId(modalType) - - return ( - setOpen(false)} - title={textMap[modalType].modalTitle} - icon={} - > - - - setModalType(option as TrendingRewardsModalType) - } - /> - - - {textMap[modalType].title} - - - {messages.winners} - - - - - {messages.lastWeek} - - - {showSpinner && ( - - - - )} - setShowSpinner(false)} - options={{ - theme: shouldUseDarkTwitter() ? 'dark' : 'light', - cards: 'none', - conversation: 'none', - hide_thread: true, - width: isMobile ? 300 : 500, - height: isMobile ? 330 : 360 - }} - /> - - - - - - - {messages.terms} - - - - - - ) -} - -export default TrendingRewardsModal diff --git a/packages/web/src/pages/modals/Modals.tsx b/packages/web/src/pages/modals/Modals.tsx index 434652b1276..8b856e639a9 100644 --- a/packages/web/src/pages/modals/Modals.tsx +++ b/packages/web/src/pages/modals/Modals.tsx @@ -70,10 +70,6 @@ const CreateChatModal = lazy( () => import('pages/chat-page/components/CreateChatModal') ) -const TrendingRewardsModal = lazy( - () => import('components/rewards/modals/TrendingRewardsModal') -) - const InboxSettingsModal = lazy( () => import('components/inbox-settings-modal/InboxSettingsModal') ) @@ -107,7 +103,6 @@ const commonModalsMap: { [Modal in ModalTypes]?: ComponentType } = { LabelAccount: LabelAccountModal, LockedContent: LockedContentModal, APIRewardsExplainer: TopAPIModal, - TrendingRewardsExplainer: TrendingRewardsModal, ChallengeRewards: ChallengeRewardsModal, ClaimAllRewards: ClaimAllRewardsModal, ClaimVestedCoinsModal, diff --git a/packages/web/src/pages/rewards-page/RewardsPage.tsx b/packages/web/src/pages/rewards-page/RewardsPage.tsx index 833278053bb..afd3ddbc1d6 100644 --- a/packages/web/src/pages/rewards-page/RewardsPage.tsx +++ b/packages/web/src/pages/rewards-page/RewardsPage.tsx @@ -20,7 +20,6 @@ import { BASE_URL } from 'utils/route' import styles from './RewardsPage.module.css' import Tiers from './Tiers' import { ChallengeRewardsTile } from './components/ChallengeRewards/ChallengeRewardsTile' -import { TrendingRewardsTile } from './components/TrendingRewards/TrendingRewardsTile' const { REWARDS_PAGE, TRENDING_PAGE } = route const messages = { @@ -45,7 +44,6 @@ const RewardsContent = () => { ) : null} - ) diff --git a/packages/web/src/pages/rewards-page/RewardsTile.module.css b/packages/web/src/pages/rewards-page/RewardsTile.module.css index 0ae4b0ecbec..b88162a9f13 100644 --- a/packages/web/src/pages/rewards-page/RewardsTile.module.css +++ b/packages/web/src/pages/rewards-page/RewardsTile.module.css @@ -1,8 +1,9 @@ .rewardsTile { background-color: var(--harmony-white); margin-bottom: 32px; - padding: 42px 28px 32px; + padding: 32px 28px 32px; } + .title { background: var(--harmony-gradient); background-clip: text; @@ -68,11 +69,12 @@ font-size: var(--harmony-font-2xl); font-weight: var(--harmony-font-bold); margin-bottom: 8px; - text-align: left; } + .rewardTitle i { margin-right: 8px; } + .rewardTitle.mobile { font-size: var(--harmony-font-l); } @@ -83,6 +85,7 @@ margin-bottom: 24px; align-items: center; } + .rewardProgress.mobile { align-items: flex-start; flex-direction: row; @@ -96,6 +99,7 @@ text-transform: uppercase; margin-right: 16px; } + .rewardProgressBar { flex: 1 1 0; } @@ -109,6 +113,7 @@ padding: 1px; margin-right: 10px; } + .iconCheck path { fill: var(--harmony-n-400); } diff --git a/packages/web/src/pages/rewards-page/Tiers.tsx b/packages/web/src/pages/rewards-page/Tiers.tsx index a71d2431166..8ae60d6ddab 100644 --- a/packages/web/src/pages/rewards-page/Tiers.tsx +++ b/packages/web/src/pages/rewards-page/Tiers.tsx @@ -39,7 +39,7 @@ import styles from './Tiers.module.css' const { show } = musicConfettiActions const messages = { - title: 'Reward Perks', + title: 'Perks', subtitle: 'Keep $AUDIO in your wallet to enjoy perks and exclusive features.', noTier: 'No tier', currentTier: 'CURRENT TIER', @@ -157,10 +157,12 @@ const TierBox = ({ tier, message }: { tier: BadgeTier; message?: string }) => { const TierColumn = ({ tier, current, + isNextTier, onClickDiscord }: { tier: BadgeTier current?: boolean + isNextTier?: boolean onClickDiscord: () => void }) => { const { color } = useTheme() @@ -177,7 +179,10 @@ const TierColumn = ({ css={{ overflow: 'hidden', minWidth: '120px', - '@media (max-width: 1100px)': { + '@media (max-width: 1200px)': { + display: current || isNextTier ? 'flex' : 'none' + }, + '@media (max-width: 1000px)': { display: current ? 'flex' : 'none' } }} @@ -269,6 +274,7 @@ const TierTable = ({ tier: BadgeTier onClickDiscord: () => void }) => { + const tiers = ['none', 'bronze', 'silver', 'gold', 'platinum'] as BadgeTier[] return ( @@ -287,17 +293,16 @@ const TierTable = ({ ))} - {(['none', 'bronze', 'silver', 'gold', 'platinum'] as BadgeTier[]).map( - (displayTier) => ( - - - - ) - )} + {tiers.map((displayTier) => ( + + + + ))} ) } diff --git a/packages/web/src/pages/rewards-page/components/ChallengeRewards/ChallengeRewardsTile.tsx b/packages/web/src/pages/rewards-page/components/ChallengeRewards/ChallengeRewardsTile.tsx index 080ebbea7f1..5ba58a9f9d8 100644 --- a/packages/web/src/pages/rewards-page/components/ChallengeRewards/ChallengeRewardsTile.tsx +++ b/packages/web/src/pages/rewards-page/components/ChallengeRewards/ChallengeRewardsTile.tsx @@ -1,23 +1,40 @@ import { useEffect, useMemo, useState } from 'react' import { useCurrentAccountUser, useCurrentAccount } from '@audius/common/api' -import { ChallengeName } from '@audius/common/src/models/AudioRewards' +import { + ChallengeName, + ChallengeRewardID +} from '@audius/common/src/models/AudioRewards' +import { SETTINGS_PAGE } from '@audius/common/src/utils/route' import { audioRewardsPageActions, audioRewardsPageSelectors, ChallengeRewardsModalType, challengesSelectors, - CommonState + CommonState, + useTierAndVerifiedForUser } from '@audius/common/store' import { - makeOptimisticChallengeSortComparator, - removeNullable + isRewardOpenToAll, + makeOptimisticChallengeSortComparator } from '@audius/common/utils' -import { Box, Flex, Text } from '@audius/harmony' +import { + Box, + Flex, + IconCaretRight, + IconLock, + IconVerified, + PlainButton, + SelectablePill, + Text, + useTheme +} from '@audius/harmony' import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router' import { useSetVisibility } from 'common/hooks/useModalState' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' +import { useIsMobile } from 'hooks/useIsMobile' import { useWithMobileStyle } from 'hooks/useWithMobileStyle' import { getChallengeConfig } from 'pages/rewards-page/config' @@ -27,7 +44,6 @@ import { ClaimAllRewardsPanel } from '../ClaimAllRewardsPanel' import { Tile } from '../Tile' import { RewardPanel } from './RewardPanel' -import { useRewardIds } from './hooks/useRewardIds' const { getUserChallenges, getUserChallengesLoading } = audioRewardsPageSelectors @@ -52,12 +68,10 @@ export const ChallengeRewardsTile = ({ getOptimisticUserChallenges(state, currentAccount, currentUser) ) const [haveChallengesLoaded, setHaveChallengesLoaded] = useState(false) - - // The referred challenge only needs a tile if the user was referred - const hideReferredTile = !userChallenges[ChallengeName.Referred]?.is_complete - const rewardIds = useRewardIds({ - [ChallengeName.Referred]: hideReferredTile - }) + const [showCompleted, setShowCompleted] = useState(false) + const navigate = useNavigate() + const { spacing } = useTheme() + const { isVerified } = useTierAndVerifiedForUser(currentUser?.user_id) useEffect(() => { if (!userChallengesLoading && !haveChallengesLoaded) { @@ -75,40 +89,228 @@ export const ChallengeRewardsTile = ({ setVisibility('ChallengeRewards')(true) } - const rewardIdsSorted = useMemo( - () => - rewardIds - // Filter out challenges that DN didn't return - .map((id) => userChallenges[id]?.challenge_id) - .filter(removeNullable) - .sort(makeOptimisticChallengeSortComparator(optimisticUserChallenges)), - [rewardIds, optimisticUserChallenges, userChallenges] - ) + const rewardIdsSorted = useMemo(() => { + // Get all challenge IDs directly from userChallenges (from API) + // userChallenges is keyed by challenge_id + const allRewardIds = Object.keys(userChallenges).filter((id) => { + const challengeId = id as ChallengeRewardID + // The referred challenge only needs a tile if the user was referred + if (challengeId === ChallengeName.Referred) { + return userChallenges[challengeId]?.is_complete === true + } + // Include all other challenges + return true + }) as ChallengeRewardID[] + + return allRewardIds.sort( + makeOptimisticChallengeSortComparator(optimisticUserChallenges) + ) + }, [optimisticUserChallenges, userChallenges]) + + // Filter completed rewards based on toggle + const filteredRewardIds = useMemo(() => { + if (showCompleted) { + return rewardIdsSorted + } + return rewardIdsSorted.filter((id) => { + const challenge = optimisticUserChallenges[id] + if (!challenge) return true + const hasDisbursed = + challenge.state === 'disbursed' || + (challenge.challenge_id === ChallengeName.OneShot && + challenge.disbursed_amount > 0) + return !hasDisbursed + }) + }, [rewardIdsSorted, optimisticUserChallenges, showCompleted]) + + // When verified, combine all rewards and sort by claimability + // When not verified, separate into open-to-all and verified-only + const { allRewardsSorted, openToAllRewards, verifiedOnlyRewards } = + useMemo(() => { + if (isVerified) { + // When verified, combine all rewards and sort by claimability + const allRewards = [...filteredRewardIds].sort( + makeOptimisticChallengeSortComparator(optimisticUserChallenges) + ) + return { + allRewardsSorted: allRewards, + openToAllRewards: [], + verifiedOnlyRewards: [] + } + } else { + // When not verified, separate into open-to-all and verified-only + const openToAll: typeof filteredRewardIds = [] + const verifiedOnly: typeof filteredRewardIds = [] + + filteredRewardIds.forEach((id) => { + if (isRewardOpenToAll(id)) { + openToAll.push(id) + } else { + verifiedOnly.push(id) + } + }) + + return { + allRewardsSorted: [], + openToAllRewards: openToAll, + verifiedOnlyRewards: verifiedOnly + } + } + }, [filteredRewardIds, isVerified, optimisticUserChallenges]) + + const hasLockedRewards = !isVerified && verifiedOnlyRewards.length > 0 + + // When verified, render all rewards together sorted by claimability + const allRewardsTiles = isVerified + ? allRewardsSorted.map((id) => { + const props = getChallengeConfig(id) + return + }) + : [] + + const openToAllTiles = !isVerified + ? openToAllRewards.map((id) => { + const props = getChallengeConfig(id) + return + }) + : [] - const rewardsTiles = rewardIdsSorted.map((id) => { - const props = getChallengeConfig(id) - return - }) + const verifiedOnlyTiles = !isVerified + ? verifiedOnlyRewards.map((id) => { + const props = getChallengeConfig(id) + return + }) + : [] const wm = useWithMobileStyle(styles.mobile) + const isMobile = useIsMobile() return ( - - {messages.title} - - - - {messages.description1} - - + + + + {messages.title} + + + {messages.description1} + + + + setShowCompleted(!showCompleted)} + /> + + {userChallengesLoading && !haveChallengesLoaded ? ( ) : ( <> -
{rewardsTiles}
+ {isVerified && allRewardsTiles.length > 0 && ( +
{allRewardsTiles}
+ )} + {!isVerified && openToAllTiles.length > 0 && ( +
{openToAllTiles}
+ )} + {hasLockedRewards && ( + +
+ {verifiedOnlyTiles} +
+ + + + Required + + + + + + + + Get verified for access to even more rewards! + + { + e.stopPropagation() + navigate(SETTINGS_PAGE) + }} + iconRight={IconCaretRight} + > + Settings + + +
+ )} + {!hasLockedRewards && verifiedOnlyTiles.length > 0 && ( +
{verifiedOnlyTiles}
+ )} )}
diff --git a/packages/web/src/pages/rewards-page/components/ChallengeRewards/RewardPanel.tsx b/packages/web/src/pages/rewards-page/components/ChallengeRewards/RewardPanel.tsx index 6cb1eb88164..5aec2e38a82 100644 --- a/packages/web/src/pages/rewards-page/components/ChallengeRewards/RewardPanel.tsx +++ b/packages/web/src/pages/rewards-page/components/ChallengeRewards/RewardPanel.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react' + import { useCurrentAccount, useCurrentAccountUser } from '@audius/common/api' import { useFormattedProgressLabel } from '@audius/common/hooks' import { @@ -12,14 +14,13 @@ import { CommonState, challengesSelectors } from '@audius/common/store' -import { isNewChallenge } from '@audius/common/utils' +import { getChallengeStatusLabel } from '@audius/common/utils' import { Box, Flex, IconCheck, IconHeadphones, Paper, - ProgressBar, Text, useTheme } from '@audius/harmony' @@ -27,6 +28,7 @@ import { useSelector } from 'react-redux' import { useLocation } from 'react-router' import { useEffectOnce } from 'react-use' +import { CoinProgressBar } from 'components/coin-progress-bar/CoinProgressBar' import { make, track } from 'services/analytics' import { doesMatchRoute } from 'utils/route' @@ -61,7 +63,6 @@ export const RewardPanel = ({ getOptimisticUserChallenges(state, currentAccount, currentUser) ) const location = useLocation() - const openRewardModal = () => { openModal(id) track( @@ -87,11 +88,6 @@ export const RewardPanel = ({ challenge.max_steps > 1 && challenge.challenge_type !== 'aggregate' && !hasDisbursed - const shouldShowNewChallengePill = - (challenge?.challenge_id && - isNewChallenge(challenge?.challenge_id) && - !needsDisbursement) ?? - false const formattedProgressLabel: string = useFormattedProgressLabel({ challenge, @@ -99,10 +95,35 @@ export const RewardPanel = ({ remainingLabel }) + // Determine the final label to display + // If there's no progress bar, no "Ready to Claim" status, and the formatted label is empty or not meaningful, + // show "Available" instead + const displayLabel = useMemo(() => { + // If there's a progress bar or "Ready to Claim" status, use the formatted label + if (shouldShowProgressBar || needsDisbursement) { + return formattedProgressLabel + } + + // If the formatted label is empty or just whitespace, use getChallengeStatusLabel + // which will return "Available" for challenges without meaningful status + if (!formattedProgressLabel || formattedProgressLabel.trim() === '') { + return getChallengeStatusLabel(challenge, id) + } + + // Otherwise use the formatted label + return formattedProgressLabel + }, [ + formattedProgressLabel, + shouldShowProgressBar, + needsDisbursement, + challenge, + id + ]) + return ( @@ -118,12 +141,9 @@ export const RewardPanel = ({ justifyContent='flex-end' p='s' w='100%' - css={{ position: 'absolute' }} + css={{ position: 'absolute', zIndex: 2 }} > - + @@ -144,14 +164,14 @@ export const RewardPanel = ({ ) : null} - {formattedProgressLabel} + {displayLabel} {shouldShowProgressBar && challenge.max_steps && ( - )} diff --git a/packages/web/src/pages/rewards-page/components/ChallengeRewards/StatusPill.tsx b/packages/web/src/pages/rewards-page/components/ChallengeRewards/StatusPill.tsx index a3efe3768ba..f06b132644f 100644 --- a/packages/web/src/pages/rewards-page/components/ChallengeRewards/StatusPill.tsx +++ b/packages/web/src/pages/rewards-page/components/ChallengeRewards/StatusPill.tsx @@ -1,4 +1,4 @@ -import { Flex, IconSparkles, Text, TextProps, useTheme } from '@audius/harmony' +import { Flex, Text, TextProps, useTheme } from '@audius/harmony' import { messages } from '../../messages' @@ -42,13 +42,9 @@ const BasePill = ({ type StatusPillProps = { shouldShowClaimPill: boolean - shouldShowNewChallengePill: boolean } -export const StatusPill = ({ - shouldShowClaimPill, - shouldShowNewChallengePill -}: StatusPillProps) => { +export const StatusPill = ({ shouldShowClaimPill }: StatusPillProps) => { const { color } = useTheme() if (shouldShowClaimPill) { @@ -63,22 +59,5 @@ export const StatusPill = ({ ) } - if (shouldShowNewChallengePill) { - return ( - - - - - {messages.new} - - - - ) - } - return null } diff --git a/packages/web/src/pages/rewards-page/components/ChallengeRewards/hooks/useRewardIds.ts b/packages/web/src/pages/rewards-page/components/ChallengeRewards/hooks/useRewardIds.ts index aaeffb29420..4e6e0ccc3f1 100644 --- a/packages/web/src/pages/rewards-page/components/ChallengeRewards/hooks/useRewardIds.ts +++ b/packages/web/src/pages/rewards-page/components/ChallengeRewards/hooks/useRewardIds.ts @@ -9,8 +9,8 @@ const validRewardIds: Set = new Set([ 'listen-streak', 'profile-completion', 'first-playlist', - ChallengeName.AudioMatchingSell, // $AUDIO matching seller - ChallengeName.AudioMatchingBuy, // $AUDIO matching buyer + ChallengeName.AudioMatchingSell, + ChallengeName.AudioMatchingBuy, ChallengeName.FirstPlaylist, ChallengeName.MobileInstall, ChallengeName.ProfileCompletion, @@ -28,7 +28,10 @@ const validRewardIds: Set = new Set([ ChallengeName.Tastemaker, ChallengeName.CommentPin, ChallengeName.Cosign, - ChallengeName.RemixContestWinner + ChallengeName.RemixContestWinner, + ChallengeName.TrendingTrack, + ChallengeName.TrendingPlaylist, + ChallengeName.TrendingUndergroundTrack ]) /** Pulls rewards from remoteconfig */ diff --git a/packages/web/src/pages/rewards-page/components/ClaimAllRewardsPanel.tsx b/packages/web/src/pages/rewards-page/components/ClaimAllRewardsPanel.tsx index da8913e65d1..88522afd1cf 100644 --- a/packages/web/src/pages/rewards-page/components/ClaimAllRewardsPanel.tsx +++ b/packages/web/src/pages/rewards-page/components/ClaimAllRewardsPanel.tsx @@ -1,124 +1,154 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' +import { useCurrentAccount, useCurrentAccountUser } from '@audius/common/api' +import { useChallengeCooldownSchedule } from '@audius/common/hooks' +import { challengesSelectors, CommonState } from '@audius/common/store' +import { formatNumberCommas } from '@audius/common/utils' import { - formatCooldownChallenges, - useChallengeCooldownSchedule -} from '@audius/common/hooks' -import { - Box, Button, + Divider, Flex, IconArrowRight as IconArrow, - IconTokenGold, + IconInfo, Paper, - PlainButton, Text, - useTheme + Tooltip } from '@audius/harmony' +import { useSelector } from 'react-redux' import { useModalState } from 'common/hooks/useModalState' import { useIsMobile } from 'hooks/useIsMobile' -import { useWithMobileStyle } from 'hooks/useWithMobileStyle' -import styles from '../RewardsTile.module.css' -import { messages } from '../messages' +const { getOptimisticUserChallenges } = challengesSelectors + +const messages = { + yourRewards: 'Your Rewards', + totalClaimed: 'Total Claimed', + pending: 'Pending', + readyToClaim: 'Ready to Claim', + claimAll: 'Claim All' +} export const ClaimAllRewardsPanel = () => { const isMobile = useIsMobile() || window.innerWidth < 1080 - const wm = useWithMobileStyle(styles.mobile) - const { cooldownChallenges, cooldownAmount, claimableAmount, isEmpty } = + const { cooldownAmount, claimableAmount, isEmpty } = useChallengeCooldownSchedule({ multiple: true }) - const claimable = claimableAmount > 0 const [, setClaimAllRewardsVisibility] = useModalState('ClaimAllRewards') - const { iconSizes } = useTheme() + const { data: currentAccount } = useCurrentAccount() + const { data: currentUser } = useCurrentAccountUser() + const optimisticUserChallenges = useSelector((state: CommonState) => + getOptimisticUserChallenges(state, currentAccount, currentUser) + ) - const onClickClaimAllRewards = useCallback(() => { - setClaimAllRewardsVisibility(true) - }, [setClaimAllRewardsVisibility]) + // Calculate total claimed amount + const totalClaimed = useMemo(() => { + return Object.values(optimisticUserChallenges).reduce( + (sum, challenge) => sum + (challenge?.disbursed_amount ?? 0), + 0 + ) + }, [optimisticUserChallenges]) + + // Pending amount is the cooldown amount + const pendingAmount = cooldownAmount - const onClickMoreInfo = useCallback(() => { + const onClickClaimAllRewards = useCallback(() => { setClaimAllRewardsVisibility(true) }, [setClaimAllRewardsVisibility]) - const handleClick = useCallback(() => { - if (claimable) { - onClickClaimAllRewards() - } else if (cooldownAmount > 0) { - onClickMoreInfo() - } - }, [claimable, cooldownAmount, onClickClaimAllRewards, onClickMoreInfo]) - if (isEmpty) return null + const tooltipMessages = { + totalClaimed: 'Total amount of $AUDIO you have claimed from all rewards', + pending: 'Amount of $AUDIO pending in cooldown period', + readyToClaim: 'Amount of $AUDIO ready to claim now' + } + if (isMobile) { return ( - - - {claimable ? ( - - ) : null} - {isEmpty ? null : ( - - {claimable - ? messages.totalReadyToClaim - : messages.totalUpcomingRewards} - - )} + + {messages.yourRewards} + + + + + + + {formatNumberCommas(totalClaimed)} + + + $AUDIO + + + + + {messages.totalClaimed} + + + + + + + + + + + {formatNumberCommas(pendingAmount)} + + + $AUDIO + + + + + {messages.pending} + + + + + + - {cooldownAmount > 0 ? ( - - - {messages.formatCooldownAmount(cooldownAmount)} + {/* Second row: Ready to Claim */} + + + + {formatNumberCommas(claimableAmount)} + + + $AUDIO - - ) : null} - - - {claimable - ? messages.formatClaimableAmount(claimableAmount) - : messages.availableMessage( - formatCooldownChallenges(cooldownChallenges) - )} - - - {claimable ? ( - - ) : cooldownAmount > 0 ? ( - - {messages.moreInfo} - - ) : null} + + + + {messages.readyToClaim} + + + + + + + {claimableAmount > 0 ? ( + + ) : null} ) } @@ -127,59 +157,108 @@ export const ClaimAllRewardsPanel = () => { - - {claimableAmount > 0 ? ( - - ) : null} - - - {isEmpty ? null : ( - - {claimableAmount > 0 - ? messages.totalReadyToClaim - : messages.totalUpcomingRewards} + + {messages.yourRewards} + + + + + + + {formatNumberCommas(totalClaimed)} + + + $AUDIO + + + + + {messages.totalClaimed} + + + + + + + + + + + {formatNumberCommas(pendingAmount)} + + + $AUDIO - )} - {cooldownAmount > 0 ? ( -
- - {messages.formatCooldownAmount(cooldownAmount)} - -
- ) : null} +
+ + + {messages.pending} + + + + + +
+ + + + + + + {formatNumberCommas(claimableAmount)} + + + $AUDIO + + + + + {messages.readyToClaim} + + + + + + + {claimableAmount > 0 ? ( + + ) : null} + - - {claimableAmount > 0 - ? messages.formatClaimableAmount(claimableAmount) - : messages.availableMessage( - formatCooldownChallenges(cooldownChallenges) - )} -
- {claimableAmount > 0 ? ( - - ) : cooldownAmount > 0 ? ( - - {messages.moreInfo} - - ) : null}
) } diff --git a/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/TrendingRewardsModalContent.tsx b/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/TrendingRewardsModalContent.tsx new file mode 100644 index 00000000000..50eee157242 --- /dev/null +++ b/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/TrendingRewardsModalContent.tsx @@ -0,0 +1,84 @@ +import { useCurrentAccount, useCurrentAccountUser } from '@audius/common/api' +import { + audioRewardsPageSelectors, + challengesSelectors, + ClaimStatus, + CommonState +} from '@audius/common/store' +import { + challengeRewardsConfig, + getChallengeStatusLabel +} from '@audius/common/utils' +import { Flex, Text } from '@audius/harmony' +import { useSelector } from 'react-redux' + +import { ChallengeRewardsLayout } from './ChallengeRewardsLayout' +import { ClaimButton } from './ClaimButton' +import { type DefaultChallengeProps } from './types' + +const { getOptimisticUserChallenges } = challengesSelectors +const { getUndisbursedUserChallenges, getClaimStatus } = + audioRewardsPageSelectors + +export const TrendingRewardsModalContent = ({ + challenge, + challengeName, + onNavigateAway, + errorContent +}: DefaultChallengeProps) => { + const { data: currentAccount } = useCurrentAccount() + const { data: currentUser } = useCurrentAccountUser() + const userChallenge = useSelector((state: CommonState) => + getOptimisticUserChallenges(state, currentAccount, currentUser) + )[challengeName] + const undisbursedUserChallenges = useSelector(getUndisbursedUserChallenges) + + const claimStatus = useSelector(getClaimStatus) + const claimInProgress = + claimStatus === ClaimStatus.CLAIMING || + claimStatus === ClaimStatus.WAITING_FOR_RETRY + + const { fullDescription } = challengeRewardsConfig[challengeName] ?? { + fullDescription: () => '' + } + + const progressStatusLabel = ( + + + {userChallenge + ? getChallengeStatusLabel(userChallenge, challengeName) + : 'AVAILABLE'} + + + ) + + return ( + + {fullDescription?.(userChallenge ?? challenge)} + + } + amount={userChallenge?.totalAmount ?? challenge?.totalAmount} + progressStatusLabel={progressStatusLabel} + actions={ + + } + errorContent={errorContent} + /> + ) +} diff --git a/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/challengeContentRegistry.ts b/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/challengeContentRegistry.ts index d69692d7d97..2c7452bea77 100644 --- a/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/challengeContentRegistry.ts +++ b/packages/web/src/pages/rewards-page/components/modals/ChallengeRewardsModal/challengeContentRegistry.ts @@ -11,6 +11,7 @@ import { PlayCountMilestoneContent } from './PlayCountMilestoneContent' import { ReferralsChallengeModalContent } from './ReferralsChallengeModalContent' import { RemixContestWinnerChallengeModalContent } from './RemixContestWinnerChallengeModalContent' import { TastemakerChallengeModalContent } from './TastemakerChallengeModalContent' +import { TrendingRewardsModalContent } from './TrendingRewardsModalContent' import { type ChallengeContentMap, type ChallengeContentComponent @@ -45,6 +46,12 @@ export const challengeContentRegistry: ChallengeContentMap = { PinnedCommentChallengeModalContent as ChallengeContentComponent, [ChallengeName.RemixContestWinner]: RemixContestWinnerChallengeModalContent as ChallengeContentComponent, + [ChallengeName.TrendingTrack]: + TrendingRewardsModalContent as ChallengeContentComponent, + [ChallengeName.TrendingPlaylist]: + TrendingRewardsModalContent as ChallengeContentComponent, + [ChallengeName.TrendingUndergroundTrack]: + TrendingRewardsModalContent as ChallengeContentComponent, default: DefaultChallengeContent as ChallengeContentComponent } diff --git a/packages/web/src/pages/rewards-page/messages.tsx b/packages/web/src/pages/rewards-page/messages.tsx index 07c77fcf7f3..50917e12be9 100644 --- a/packages/web/src/pages/rewards-page/messages.tsx +++ b/packages/web/src/pages/rewards-page/messages.tsx @@ -9,7 +9,7 @@ type ClaimableSummaryTableItem = SummaryTableItem & { } export const messages = { - title: 'Achievement Rewards', + title: 'Rewards', description1: 'Earn $AUDIO by completing simple tasks while using Audius.', completeLabel: 'COMPLETE', claimReward: 'Claim This Reward', diff --git a/packages/web/src/pages/trending-page/components/RewardsBanner.tsx b/packages/web/src/pages/trending-page/components/RewardsBanner.tsx deleted file mode 100644 index 340cb1d0ec9..00000000000 --- a/packages/web/src/pages/trending-page/components/RewardsBanner.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useCallback } from 'react' - -import { - audioRewardsPageActions, - TrendingRewardsModalType -} from '@audius/common/store' -import { - IconCaretRight, - IconCrown, - Flex, - Text, - useTheme, - PlainButton, - Paper -} from '@audius/harmony' -import { useDispatch } from 'react-redux' - -import { useModalState } from 'common/hooks/useModalState' -import { useIsMobile } from 'hooks/useIsMobile' - -const { setTrendingRewardsModalType } = audioRewardsPageActions - -const messages = { - learnMore: 'Learn More' -} - -const messageMap = { - tracks: { - title: 'Global Trending: Weekly Top 5', - description: 'Artists trending Fridays at 12PM PT win tokens!' - }, - playlists: { - title: 'Trending Playlists: Weekly Top 5', - description: 'Playlists trending Fridays at 12PM PT win tokens!' - }, - underground: { - title: 'Underground Trending: Weekly Top 5', - description: 'Artists trending Fridays at 12PM PT win tokens!' - } -} - -type RewardsBannerProps = { - bannerType: 'tracks' | 'playlists' | 'underground' -} - -const useHandleBannerClick = () => { - const [, setModal] = useModalState('TrendingRewardsExplainer') - const dispatch = useDispatch() - const onClickBanner = useCallback( - (modalType: TrendingRewardsModalType) => { - setModal(true) - dispatch(setTrendingRewardsModalType({ modalType })) - }, - [dispatch, setModal] - ) - return onClickBanner -} - -const RewardsBanner = ({ bannerType }: RewardsBannerProps) => { - const isMobile = useIsMobile() - const onClick = useHandleBannerClick() - const { spacing, color } = useTheme() - - return ( - onClick(bannerType)} - pv='m' - ph='2xl' - css={{ - background: color.special.gradient - }} - > - - - - - {messageMap[bannerType].title} - - - - {messageMap[bannerType].description} - - - {!isMobile && ( - - {messages.learnMore} - - )} - - ) -} - -export default RewardsBanner diff --git a/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx b/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx index 674225f78e4..3de76a6fb77 100644 --- a/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx +++ b/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx @@ -19,8 +19,6 @@ import { useTrendingPageCleanup } from 'pages/trending-page/hooks/useTrendingPag import { useTrendingPageState } from 'pages/trending-page/hooks/useTrendingPageState' import { useTrendingUrlParams } from 'pages/trending-page/hooks/useTrendingUrlParams' -import RewardsBanner from '../RewardsBanner' - import GenreSelectionModal from './GenreSelectionModal' import { TrendingGenreFilters } from './TrendingGenreFilters' import styles from './TrendingPageContent.module.css' @@ -219,11 +217,6 @@ const TrendingPageContent = ({ containerRef }: TrendingPageContentProps) => { key={`weekly-trending-tracks-${trendingGenre}`} className={styles.lineupContainer} > - {trendingGenre === null ? ( -
- -
- ) : null} { return [ <> - {trendingGenre === null ? ( -
- -
- ) : null} -
- -
) @@ -84,9 +80,6 @@ const MobileTrendingPlaylistPage = ({ hasDefaultHeader >
-
- -
diff --git a/packages/web/src/pages/trending-underground/TrendingUndergroundPage.tsx b/packages/web/src/pages/trending-underground/TrendingUndergroundPage.tsx index d9ea155f9f7..e49672745fe 100644 --- a/packages/web/src/pages/trending-underground/TrendingUndergroundPage.tsx +++ b/packages/web/src/pages/trending-underground/TrendingUndergroundPage.tsx @@ -15,7 +15,6 @@ import { LineupVariant } from 'components/lineup/types' import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' import Page from 'components/page/Page' import { useIsMobile } from 'hooks/useIsMobile' -import RewardsBanner from 'pages/trending-page/components/RewardsBanner' import { getExploreInfo } from 'ssr/metaTags' import { BASE_URL } from 'utils/route' @@ -57,9 +56,6 @@ const MobileTrendingUndergroundPage = ({ hasDefaultHeader >
-
- -
@@ -80,9 +76,6 @@ const DesktopTrendingUndergroundPage = ({ size='large' header={header} > -
- -
)