diff --git a/packages/common/src/store/challenges/selectors/optimistic-challenges.ts b/packages/common/src/store/challenges/selectors/optimistic-challenges.ts index 42dcfce0660..4a004919fb3 100644 --- a/packages/common/src/store/challenges/selectors/optimistic-challenges.ts +++ b/packages/common/src/store/challenges/selectors/optimistic-challenges.ts @@ -111,11 +111,10 @@ const toOptimisticChallenge = ( claimableAmount = undisbursed .filter(isCooldownChallengeClaimable) .reduce((acc, val) => acc + val.amount, 0) - } else if ( - challengeOverridden.challenge_type !== 'aggregate' && - state === 'completed' - ) { - claimableAmount = totalAmount + } else if (challenge.challenge_type === 'aggregate') { + claimableAmount = undisbursed.reduce((acc, val) => acc + val.amount, 0) + } else if (state === 'completed') { + claimableAmount = challenge.amount } const undisbursedSpecifiers = undisbursed.reduce( diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx index 8384f26f71d..a2db4b1d4a0 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/AudioMatchingRewardsModalContent.tsx @@ -143,6 +143,22 @@ export const AudioMatchingRewardsModalContent = ({ onNavigateAway() }, [challengeName, onNavigateAway, navigateToPage]) + const formatLabel = useCallback((item: any) => { + const { label, claimableDate, isClose } = item + const formattedLabel = isClose ? ( + label + ) : ( + + {label}  + {claimableDate.format('(M/D)')} + + ) + return { + ...item, + label: formattedLabel + } + }, []) + return (
{isMobile ? ( @@ -165,7 +181,9 @@ export const AudioMatchingRewardsModalContent = ({ {!isCooldownChallengesEmpty ? ( { +export const InviteLink = ({ className, inviteLink }: InviteLinkProps) => { const wm = useWithMobileStyle(styles.mobile) const onButtonClick = useCallback(() => { @@ -141,16 +150,16 @@ const InviteLink = ({ className, inviteLink }: InviteLinkProps) => { placement={ComponentPlacement.TOP} mount={MountPlacement.PARENT} > - - -
{messages.inviteLabel}
-
- } - /> +
+ +
@@ -229,21 +238,49 @@ type BodyProps = { } const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { + const { isEnabled: isRewardsCooldownEnabled } = useFeatureFlag( + FeatureFlags.REWARDS_COOLDOWN + ) + const { toast } = useContext(ToastContext) + const claimStatus = useSelector(getClaimStatus) + const aaoErrorCode = useSelector(getAAOErrorCode) + const claimInProgress = + claimStatus === ClaimStatus.CLAIMING || + claimStatus === ClaimStatus.WAITING_FOR_RETRY + const undisbursedUserChallenges = useSelector(getUndisbursedUserChallenges) const [modalType] = useRewardsModalType() const userHandle = useSelector(getUserHandle) const dispatch = useDispatch() const wm = useWithMobileStyle(styles.mobile) const isMobile = useIsMobile() - const userChallenges = useSelector(getOptimisticUserChallenges) const challenge = userChallenges[modalType] - const undisbursedUserChallenges = useSelector(getUndisbursedUserChallenges) - + const isCooldownChallenge = challenge && challenge.cooldown_days > 0 + const currentStepCount = challenge?.current_step_count || 0 const { fullDescription, progressLabel, isVerifiedChallenge } = challengeRewardsConfig[modalType] const { modalButtonInfo } = getChallengeConfig(modalType) + const { + cooldownChallenges, + summary, + isEmpty: isCooldownChallengesEmpty + } = useChallengeCooldownSchedule({ challengeId: challenge?.challenge_id }) - const currentStepCount = challenge?.current_step_count || 0 + // We could just depend on undisbursedAmount here + // But DN may have not indexed the challenge so check for client-side completion too + // Note that we can't handle aggregate challenges optimistically + let audioToClaim = 0 + let audioClaimedSoFar = 0 + if (challenge?.challenge_type === 'aggregate') { + audioToClaim = challenge.claimableAmount + audioClaimedSoFar = challenge.disbursed_amount + } else if (challenge?.state === 'completed') { + audioToClaim = challenge.totalAmount + audioClaimedSoFar = 0 + } else if (challenge?.state === 'disbursed') { + audioToClaim = 0 + audioClaimedSoFar = challenge.totalAmount + } let linkType: 'complete' | 'inProgress' | 'incomplete' if (challenge?.state === 'completed') { @@ -256,36 +293,33 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { const buttonInfo = modalButtonInfo?.[linkType] ?? null const buttonLink = buttonInfo?.link(userHandle) - const goToRoute = useCallback(() => { - if (!buttonLink) return - dispatch(pushRoute(buttonLink)) - dismissModal() - }, [buttonLink, dispatch, dismissModal]) + const showProgressBar = + challenge && + challenge.max_steps > 1 && + challenge.challenge_type !== 'aggregate' - const progressDescription = ( - - - {messages.verifiedChallenge} - - ) : ( - 'Task' - ) - } - description={fullDescription?.(challenge)} - /> + const progressDescriptionLabel = isVerifiedChallenge ? ( +
+ + {messages.verifiedChallenge} +
+ ) : ( + 'Task' ) - - const progressReward = ( - + const progressDescription = isRewardsCooldownEnabled ? ( +
+ {fullDescription?.(challenge)} + {isCooldownChallenge ? ( + + {messages.cooldownDescription} + + ) : null} +
+ ) : ( + fullDescription?.(challenge) ) - const progressStatusLabel = ( + const renderProgressStatusLabel = () => (
{

Incomplete

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

Complete

+ + +

Complete

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

@@ -312,33 +349,25 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => {

) - const { toast } = useContext(ToastContext) - const claimStatus = useSelector(getClaimStatus) - const aaoErrorCode = useSelector(getAAOErrorCode) - const claimInProgress = - claimStatus === ClaimStatus.CLAIMING || - claimStatus === ClaimStatus.WAITING_FOR_RETRY + const inviteLink = useMemo( + () => (userHandle ? fillString(messages.inviteLink, userHandle) : ''), + [userHandle] + ) - // We could just depend on undisbursedAmount here - // But DN may have not indexed the challenge so check for client-side completion too - // Note that we can't handle aggregate challenges optimistically - let audioToClaim = 0 - let audioClaimedSoFar = 0 - if (challenge?.challenge_type === 'aggregate') { - audioToClaim = challenge.claimableAmount - audioClaimedSoFar = challenge.disbursed_amount - } else if (challenge?.state === 'completed') { - audioToClaim = challenge.totalAmount - audioClaimedSoFar = 0 - } else if (challenge?.state === 'disbursed') { - audioToClaim = 0 - audioClaimedSoFar = challenge.totalAmount - } + const errorContent = + claimStatus === ClaimStatus.ERROR ? ( +
{getErrorMessage(aaoErrorCode)}
+ ) : null - const showProgressBar = - challenge && - challenge.max_steps > 1 && - challenge.challenge_type !== 'aggregate' + useEffect(() => { + if (claimStatus === ClaimStatus.SUCCESS) { + toast(messages.rewardClaimed, CLAIM_REWARD_TOAST_TIMEOUT_MILLIS) + dispatch(showConfetti()) + } + if (claimStatus === ClaimStatus.ALREADY_CLAIMED) { + toast(messages.rewardAlreadyClaimed, CLAIM_REWARD_TOAST_TIMEOUT_MILLIS) + } + }, [claimStatus, toast, dispatch]) const onClaimRewardClicked = useCallback(() => { if (challenge) { @@ -363,43 +392,129 @@ const ChallengeRewardsBody = ({ dismissModal }: BodyProps) => { } }, [challenge, dispatch, undisbursedUserChallenges]) - useEffect(() => { - if (claimStatus === ClaimStatus.SUCCESS) { - toast(messages.rewardClaimed, CLAIM_REWARD_TOAST_TIMEOUT_MILLIS) - dispatch(showConfetti()) + const goToRoute = useCallback(() => { + if (!buttonLink) return + dispatch(pushRoute(buttonLink)) + dismissModal() + }, [buttonLink, dispatch, dismissModal]) + + const formatLabel = useCallback((item: any) => { + const { label, claimableDate, isClose } = item + const formattedLabel = isClose ? ( + label + ) : ( + + {label}  + {claimableDate.format('(M/D)')} + + ) + return { + ...item, + label: formattedLabel } - if (claimStatus === ClaimStatus.ALREADY_CLAIMED) { - toast(messages.rewardAlreadyClaimed, CLAIM_REWARD_TOAST_TIMEOUT_MILLIS) + }, []) + + const renderCooldownSummaryTable = () => { + if ( + isRewardsCooldownEnabled && + isCooldownChallenge && + !isCooldownChallengesEmpty + ) { + return ( + + ) } - }, [claimStatus, toast, dispatch]) + return null + } - const inviteLink = useMemo( - () => (userHandle ? fillString(messages.inviteLink, userHandle) : ''), - [userHandle] - ) + const renderProgressBar = () => { + if (showProgressBar) { + return ( +
+ {isMobile ? ( +

Progress

+ ) : modalType === 'profile-completion' ? ( + + ) : null} + +
+ ) + } + return null + } - const errorContent = - claimStatus === ClaimStatus.ERROR ? ( -
{getErrorMessage(aaoErrorCode)}
- ) : null - const { isEnabled: isRewardsCooldownEnabled } = useFeatureFlag( - FeatureFlags.REWARDS_COOLDOWN - ) - if (isRewardsCooldownEnabled && challenge && challenge.cooldown_days > 0) { - return ( - - ) - } else if (!isRewardsCooldownEnabled && isAudioMatchingChallenge(modalType)) { + const renderReferralContent = () => { + if (userHandle && (modalType === 'referrals' || modalType === 'ref-v')) { + return ( +
+ +
+ +
+ ) + } + return null + } + + const renderMobileInstallContent = () => { + if (modalType === 'mobile-install') { + return ( +
+ QR Code +
+

{messages.qrText}

+

{messages.qrSubtext}

+
+
+ ) + } + return null + } + + const renderClaimButton = () => { + if (audioToClaim > 0) { + return ( + <> +
+ {`${audioToClaim} ${messages.claimAmountLabel}`} +
+ + + ) + } + return null + } + + const renderClaimedSoFarContent = () => { + if (audioClaimedSoFar > 0 && challenge?.state !== 'disbursed') { + return ( +
+ {`${formatNumberCommas(audioClaimedSoFar)} ${messages.claimedSoFar}`} +
+ ) + } + return null + } + + if (isAudioMatchingChallenge(modalType)) { return ( {
{isMobile ? ( <> - {progressDescription} +
- {progressReward} - {showProgressBar ? ( -
-

Progress

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

{messages.qrText}

-

{messages.qrSubtext}

+ {renderProgressBar()} + {renderProgressStatusLabel()}
-
- ) : null} + {renderCooldownSummaryTable()} + + )} + {renderReferralContent()} + {renderMobileInstallContent()} {buttonLink && challenge?.state !== 'completed' ? ( ) : null} -
- {audioToClaim > 0 ? ( - <> -
- {`${audioToClaim} ${messages.claimAmountLabel}`} -
- - - ) : null} - {audioClaimedSoFar > 0 && challenge?.state !== 'disbursed' ? ( -
- {`(${formatNumberCommas(audioClaimedSoFar)} ${ - messages.claimedSoFar - })`} -
- ) : null} -
+ {audioToClaim > 0 || + (audioClaimedSoFar > 0 && challenge?.state !== 'disbursed') ? ( +
+ {renderClaimButton()} + {renderClaimedSoFarContent()} +
+ ) : null} {errorContent}
) 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 deleted file mode 100644 index 09acc67afd2..00000000000 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/CooldownRewardsModalContent.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { ReactNode, useCallback } from 'react' - -import { - formatCooldownChallenges, - useChallengeCooldownSchedule -} from '@audius/common/hooks' -import { - ChallengeName, - ChallengeRewardID, - OptimisticUserChallenge -} from '@audius/common/models' -import { challengesSelectors } from '@audius/common/store' -import { - formatNumberCommas, - challengeRewardsConfig, - isAudioMatchingChallenge -} 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 { useRewardsModalType } from './ChallengeRewardsModal' -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 { - claimableAmount, - cooldownChallenges, - summary, - isEmpty: isCooldownChallengesEmpty - } = useChallengeCooldownSchedule({ challengeId: challenge?.challenge_id }) - const userChallenge = useSelector(getOptimisticUserChallenges)[challengeName] - - const progressDescription = ( - - {fullDescription?.(challenge)} - - {messages.descriptionSubtext} - -
- } - /> - ) - const [modalType] = useRewardsModalType() - const amount = isAudioMatchingChallenge(modalType) - ? formatNumberCommas(challenge?.amount ?? '') - : formatNumberCommas(challenge?.totalAmount ?? '') - 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} -
- ) -} diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/styles.module.css b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/styles.module.css index 1a1dba6caa1..f07f7f48f0d 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/styles.module.css +++ b/packages/web/src/pages/audio-rewards-page/components/modals/ChallengeRewardsModal/styles.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; align-items: center; - gap: var(--harmony-spacing-m); + gap: var(--harmony-spacing-2xl); } .container h2 { font-weight: 900; @@ -125,6 +125,7 @@ .progressStatus { padding: 12px 32px; text-align: center; + border-top: 1px solid var(--harmony-border-strong); } .progressStatus h3 { margin-bottom: 0px; @@ -142,10 +143,7 @@ color: var(--harmony-s-200); } .progressStatus.complete { - background-color: var(--harmony-light-green); -} -.progressStatus.complete h3 { - color: var(--harmony-white); + background-color: var(--harmony-bg-surface-1); } .buttonLink { @@ -155,7 +153,6 @@ /* QR Code Styles */ .qrContainer { display: flex; - margin-top: 32px; } .qrContainer.mobile { display: none; @@ -187,46 +184,8 @@ } /* Invite Link Styles */ -.inviteButtonContainer { - gap: 10px; - cursor: pointer; - transition: all 0.07s ease-in-out; - transform: scale3d(1, 1, 1); - min-width: unset; - min-height: 60px; - padding: unset; -} .inviteButtonContainer.mobile { - padding: 16px; - margin-top: 8px; -} -.inviteButtonContainer:hover { - transform: scale3d(1.03, 1.03, 1.03); -} -.inviteButtonContainer:active { - transform: scale3d(0.99, 0.99, 0.99); -} - -.inviteLinkContainer { - display: flex; - width: 100%; - justify-content: center; - align-items: center; -} - -.inviteLink { - min-width: 0; - font-size: var(--harmony-font-m); -} - -.inviteIcon { - height: 24px; - flex: 0 0 24px; - margin-right: 8px; -} - -.inviteIcon path { - fill: var(--harmony-static-white); + margin-top: 24px; } .toastContainer { @@ -280,7 +239,9 @@ } .claimRewardWrapper { - margin-top: 32px; + display: flex; + flex-direction: column; + align-items: center; } .claimRewardWrapper.mobile { @@ -316,8 +277,9 @@ text-align: center; text-transform: uppercase; color: var(--harmony-n-400); - font-weight: var(--harmony-font-heavy); + font-weight: var(--harmony-font-bold); font-size: var(--harmony-font-s); + letter-spacing: 1px; } .claimError { @@ -351,7 +313,6 @@ display: flex; width: 100%; align-items: center; - margin-top: 32px; } .buttonContainer.mobile {