From 5978ffe26c3fba1c53d9b7df8378ef2b031e5b72 Mon Sep 17 00:00:00 2001 From: Isaac Solo Date: Fri, 29 Mar 2024 09:07:42 -0700 Subject: [PATCH] [C-4079] Add rewards cooldown to web (#7967) --- packages/common/src/models/AudioRewards.ts | 1 + .../services/remote-config/feature-flags.ts | 6 +- .../store/pages/audio-rewards/store.test.ts | 15 +- .../ChallengeRewardsModal.tsx | 230 ++++++++++-------- .../CooldownRewardsModalContent.tsx | 170 +++++++++++++ 5 files changed, 311 insertions(+), 111 deletions(-) create mode 100644 packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/CooldownRewardsModalContent.tsx diff --git a/packages/common/src/models/AudioRewards.ts b/packages/common/src/models/AudioRewards.ts index a04a34c32c9..95f26462259 100644 --- a/packages/common/src/models/AudioRewards.ts +++ b/packages/common/src/models/AudioRewards.ts @@ -12,6 +12,7 @@ export type UserChallenge = { user_id: string amount: number disbursed_amount: number + cooldown_days: number } export type Specifier = string diff --git a/packages/common/src/services/remote-config/feature-flags.ts b/packages/common/src/services/remote-config/feature-flags.ts index 595c159256f..97e4d7ca487 100644 --- a/packages/common/src/services/remote-config/feature-flags.ts +++ b/packages/common/src/services/remote-config/feature-flags.ts @@ -57,7 +57,8 @@ export enum FeatureFlags { EDIT_ALBUMS = 'edit_albums', COINFLOW_OFFRAMP_ENABLED = 'coinflow_offramp_enabled', TIKTOK_NATIVE_AUTH = 'tiktok_native_auth', - PREMIUM_ALBUMS_ENABLED = 'premium_albums_enabled' + PREMIUM_ALBUMS_ENABLED = 'premium_albums_enabled', + REWARDS_COOLDOWN = 'rewards_cooldown' } type FlagDefaults = Record @@ -130,5 +131,6 @@ export const flagDefaults: FlagDefaults = { [FeatureFlags.EDIT_ALBUMS]: false, [FeatureFlags.COINFLOW_OFFRAMP_ENABLED]: false, [FeatureFlags.TIKTOK_NATIVE_AUTH]: true, - [FeatureFlags.PREMIUM_ALBUMS_ENABLED]: false + [FeatureFlags.PREMIUM_ALBUMS_ENABLED]: false, + [FeatureFlags.REWARDS_COOLDOWN]: false } diff --git a/packages/web/src/common/store/pages/audio-rewards/store.test.ts b/packages/web/src/common/store/pages/audio-rewards/store.test.ts index 5382e0b2f5a..d9bc9ccbbdc 100644 --- a/packages/web/src/common/store/pages/audio-rewards/store.test.ts +++ b/packages/web/src/common/store/pages/audio-rewards/store.test.ts @@ -479,7 +479,8 @@ describe.skip('Rewards Page Sagas', () => { challenge_type: 'numeric', specifier: '1', user_id: '1', - disbursed_amount: 7 + disbursed_amount: 7, + cooldown_days: 0 }, { challenge_id: 'referrals', @@ -492,7 +493,8 @@ describe.skip('Rewards Page Sagas', () => { challenge_type: 'numeric', specifier: '1', user_id: '1', - disbursed_amount: 5 + disbursed_amount: 5, + cooldown_days: 0 }, { challenge_id: 'track-upload', @@ -505,7 +507,8 @@ describe.skip('Rewards Page Sagas', () => { challenge_type: 'numeric', specifier: '1', user_id: '1', - disbursed_amount: 3 + disbursed_amount: 3, + cooldown_days: 0 } ] const fetchUserChallengesProvisions: StaticProvider[] = [ @@ -614,7 +617,8 @@ describe.skip('Rewards Page Sagas', () => { challenge_type: 'numeric', specifier: '1', user_id: '1', - disbursed_amount: 7 + disbursed_amount: 7, + cooldown_days: 0 } ] @@ -667,7 +671,8 @@ describe.skip('Rewards Page Sagas', () => { challenge_type: 'numeric', specifier: '1', user_id: '1', - disbursed_amount: 2 + disbursed_amount: 2, + cooldown_days: 0 } ] diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx index 873f1aea4c0..4cf28ada717 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useContext, useMemo } from 'react' +import { useFeatureFlag } from '@audius/common/hooks' +import { FeatureFlags } from '@audius/common/services' import { accountSelectors, challengesSelectors, @@ -49,6 +51,7 @@ import PurpleBox from '../../PurpleBox' import ModalDrawer from '../ModalDrawer' import { AudioMatchingRewardsModalContent } from './AudioMatchingRewardsModalContent' +import { CooldownRewardsModalContent } from './CooldownRewardsModalContent' import { ProgressDescription } from './ProgressDescription' import { ProgressReward } from './ProgressReward' import styles from './styles.module.css' @@ -291,14 +294,13 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { challenge?.state === 'completed' || challenge?.state === 'disbursed' })} > - {challenge?.state === 'incomplete' && ( + {challenge?.state === 'incomplete' ? (

Incomplete

- )} - {(challenge?.state === 'completed' || - challenge?.state === 'disbursed') && ( + ) : null} + {challenge?.state === 'completed' || challenge?.state === 'disbursed' ? (

Complete

- )} - {challenge?.state === 'in_progress' && progressLabel && ( + ) : null} + {challenge?.state === 'in_progress' && progressLabel ? (

{fillString( progressLabel, @@ -306,7 +308,7 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { formatNumberCommas(challenge?.max_steps?.toString() ?? '') )}

- )} + ) : null} ) @@ -380,113 +382,133 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { claimStatus === ClaimStatus.ERROR ? (
{getErrorMessage(aaoErrorCode)}
) : null - - return isAudioMatchingChallenge(modalType) ? ( - - ) : ( -
- {isMobile ? ( - <> - {progressDescription} -
-
+ const { isEnabled: isRewardsCooldownEnabled } = useFeatureFlag( + FeatureFlags.REWARDS_COOLDOWN + ) + if (isRewardsCooldownEnabled && challenge && challenge.cooldown_days > 0) { + return ( + + ) + } else if (!isRewardsCooldownEnabled && isAudioMatchingChallenge(modalType)) { + return ( + + ) + } else { + return ( +
+ {isMobile ? ( + <> + {progressDescription} +
+
+ {progressReward} + {showProgressBar ? ( +
+

Progress

+ +
+ ) : null} +
+ {progressStatusLabel} +
+ {modalType === 'profile-completion' ? : null} + + ) : ( +
+
+ {progressDescription} {progressReward} - {showProgressBar ? ( -
-

Progress

- -
- ) : null}
+ {showProgressBar ? ( +
+ {modalType === 'profile-completion' ? : null} + +
+ ) : null} {progressStatusLabel}
- {modalType === 'profile-completion' && } - - ) : ( -
-
- {progressDescription} - {progressReward} -
- {showProgressBar && ( -
- {modalType === 'profile-completion' && } - -
- )} - {progressStatusLabel} -
- )} + )} - {userHandle && (modalType === 'referrals' || modalType === 'ref-v') && ( -
- -
- -
- )} - {modalType === 'mobile-install' && ( -
- QR Code -
-

{messages.qrText}

-

{messages.qrSubtext}

+ {userHandle && (modalType === 'referrals' || modalType === 'ref-v') ? ( +
+ +
+
-
- )} - {buttonLink && challenge?.state !== 'completed' && ( - - )} -
- {audioToClaim > 0 ? ( - <> -
- {`${audioToClaim} ${messages.claimAmountLabel}`} -
- - ) : null} - {audioClaimedSoFar > 0 && challenge?.state !== 'disbursed' ? ( -
- {`(${formatNumberCommas(audioClaimedSoFar)} ${ - messages.claimedSoFar - })`} + {modalType === 'mobile-install' ? ( +
+ QR Code +
+

{messages.qrText}

+

{messages.qrSubtext}

+
) : null} + {buttonLink && challenge?.state !== 'completed' ? ( + + ) : null} +
+ {audioToClaim > 0 ? ( + <> +
+ {`${audioToClaim} ${messages.claimAmountLabel}`} +
+ + + ) : null} + {audioClaimedSoFar > 0 && challenge?.state !== 'disbursed' ? ( +
+ {`(${formatNumberCommas(audioClaimedSoFar)} ${ + messages.claimedSoFar + })`} +
+ ) : null} +
+ {errorContent}
- {errorContent} -
- ) + ) + } } export const ChallengeRewardsModal = () => { diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/CooldownRewardsModalContent.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/CooldownRewardsModalContent.tsx new file mode 100644 index 00000000000..3b6ebe10775 --- /dev/null +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/CooldownRewardsModalContent.tsx @@ -0,0 +1,170 @@ +import { ReactNode, useCallback } from 'react' + +import { useAudioMatchingChallengeCooldownSchedule } from '@audius/common/hooks' +import { + ChallengeName, + ChallengeRewardID, + OptimisticUserChallenge +} from '@audius/common/models' +import { challengesSelectors } from '@audius/common/store' +import { + formatNumberCommas, + challengeRewardsConfig +} from '@audius/common/utils' +import { Button, IconComponent, Text } from '@audius/harmony' +import cn from 'classnames' +import { useSelector } from 'react-redux' + +import { SummaryTable } from 'components/summary-table' +import { useIsMobile } from 'hooks/useIsMobile' +import { useWithMobileStyle } from 'hooks/useWithMobileStyle' + +import { ProgressDescription } from './ProgressDescription' +import { ProgressReward } from './ProgressReward' +import styles from './styles.module.css' + +const { getOptimisticUserChallenges } = challengesSelectors + +type AudioMatchingChallengeName = + | ChallengeName.AudioMatchingBuy + | ChallengeName.AudioMatchingSell + +const messages = { + rewardMapping: { + [ChallengeName.AudioMatchingBuy]: '$AUDIO Every Dollar Spent', + [ChallengeName.AudioMatchingSell]: '$AUDIO Every Dollar Earned' + }, + descriptionSubtext: + 'Note: There is a 7 day waiting period from completion until you can claim your reward.', + totalClaimed: (amount: string) => `Total $AUDIO Claimed: ${amount}`, + claimAudio: (amount: string) => `Claim ${amount} $AUDIO`, + upcomingRewards: 'Upcoming Rewards', + audio: '$AUDIO' +} + +type CooldownRewardsModalContentProps = { + challenge?: OptimisticUserChallenge + challengeName: ChallengeRewardID + onClaimRewardClicked: () => void + claimInProgress?: boolean + onNavigateAway: () => void + onClickProgress: () => void + progressIcon?: IconComponent | null + progressLabel?: string + errorContent?: ReactNode +} + +/** Implements custom ChallengeRewardsContent for the cooldown challenges */ +export const CooldownRewardsModalContent = ({ + challenge, + challengeName, + onClaimRewardClicked, + claimInProgress = false, + onNavigateAway, + onClickProgress, + progressIcon, + progressLabel, + errorContent +}: CooldownRewardsModalContentProps) => { + const wm = useWithMobileStyle(styles.mobile) + const isMobile = useIsMobile() + const { fullDescription } = challengeRewardsConfig[challengeName] + const { + cooldownChallenges, + claimableAmount, + cooldownChallengesSummary, + isEmpty: isCooldownChallengesEmpty + } = useAudioMatchingChallengeCooldownSchedule(challenge?.challenge_id) + const userChallenge = useSelector(getOptimisticUserChallenges)[challengeName] + + const progressDescription = ( + + {fullDescription?.(challenge)} + + {messages.descriptionSubtext} + +
+ } + /> + ) + const progressReward = ( + + ) + + const progressStatusLabel = + userChallenge && userChallenge?.disbursed_amount > 0 ? ( +
+ + {messages.totalClaimed( + formatNumberCommas(userChallenge.disbursed_amount.toString()) + )} + +
+ ) : null + + const handleClickCTA = useCallback(() => { + onClickProgress() + onNavigateAway() + }, [onNavigateAway, onClickProgress]) + return ( +
+ {isMobile ? ( + <> + {progressDescription} +
+
{progressReward}
+ {progressStatusLabel} +
+ + ) : ( + <> +
+
+ {progressDescription} + {progressReward} +
+ {progressStatusLabel} +
+ {!isCooldownChallengesEmpty ? ( + + ) : null} + + )} + {challenge?.claimableAmount && challenge.claimableAmount > 0 ? ( + + ) : ( + + )} + {errorContent} +
+ ) +}