Skip to content

Commit

Permalink
First pass of claim all modal (#8072)
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacsolo authored Apr 10, 2024
1 parent 6d957ad commit b16bad3
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ export const useChallengeCooldownSchedule = (
return { cooldownChallenges, claimableAmount }
}

export const usePendingChallengeSchedule = () => {
const challenges = useSelector(getUndisbursedUserChallenges).map((c) => ({
...c,
createdAtDate: dayjs.utc(c.created_at)
}))

const cooldownChallenges = challenges.filter(
(c) => !isCooldownChallengeClaimable(c)
)
const claimableAmount = cooldownChallenges.reduce(
(acc, curr) => acc + curr.amount,
0
)
return { cooldownChallenges, claimableAmount }
}

const getAudioMatchingCooldownLabel = (
challenge: UndisbursedUserChallenge,
now: Dayjs
Expand All @@ -53,7 +69,7 @@ const getAudioMatchingCooldownLabel = (
return createdAt.add(cooldownDays, 'day').format('ddd (M/D)')
}

const formatAudioMatchingChallengesForCooldownSchedule = (
export const formatAudioMatchingChallengesForCooldownSchedule = (
challenges: UndisbursedUserChallenge[]
) => {
if (challenges.length === 0) return []
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/parentSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const initialState: BasicModalsState = {
TiersExplainer: { isOpen: false },
TrendingRewardsExplainer: { isOpen: false },
ChallengeRewardsExplainer: { isOpen: false },
ClaimAllRewards: { isOpen: false },
LinkSocialRewardsExplainer: { isOpen: false },
APIRewardsExplainer: { isOpen: false },
TransferAudioMobileWarning: { isOpen: false },
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/store/ui/modals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type Modals =
| 'TiersExplainer'
| 'TrendingRewardsExplainer'
| 'ChallengeRewardsExplainer'
| 'ClaimAllRewards'
| 'LinkSocialRewardsExplainer'
| 'APIRewardsExplainer'
| 'TransferAudioMobileWarning'
Expand Down
104 changes: 87 additions & 17 deletions packages/web/src/pages/audio-rewards-page/ChallengeRewardsTile.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'

import {
useFeatureFlag,
usePendingChallengeSchedule
} from '@audius/common/hooks'
import {
Name,
ChallengeName,
ChallengeRewardID,
OptimisticUserChallenge
} from '@audius/common/models'
import { StringKeys } from '@audius/common/services'
import { FeatureFlags, StringKeys } from '@audius/common/services'
import {
challengesSelectors,
audioRewardsPageSelectors,
Expand All @@ -22,15 +26,19 @@ import {
} from '@audius/common/utils'
import {
Button,
Divider,
Flex,
IconArrowRight as IconArrow,
IconCheck,
IconTokenGold,
Paper,
ProgressBar,
Text
} from '@audius/harmony'
import cn from 'classnames'
import { useDispatch, useSelector } from 'react-redux'

import { useSetVisibility } from 'common/hooks/useModalState'
import { useModalState, useSetVisibility } from 'common/hooks/useModalState'
import LoadingSpinner from 'components/loading-spinner/LoadingSpinner'
import { useIsAudioMatchingChallengesEnabled } from 'hooks/useIsAudioMatchingChallengesEnabled'
import { useRemoteVar } from 'hooks/useRemoteConfig'
Expand All @@ -51,9 +59,15 @@ const messages = {
description1: 'Complete tasks to earn $AUDIO tokens!',
completeLabel: 'COMPLETE',
claimReward: 'Claim Your Reward',
claimAllRewards: 'Claim All Rewards',

readyToClaim: 'Ready to Claim',
totalReadyToClaim: 'Total Ready To Claim',
pending: 'Pending',
viewDetails: 'View Details',
new: 'New!'
new: 'New!',
goldAudioToken: 'Gold $AUDIO token',
availableNow: '$AUDIO available now'
}

type RewardPanelProps = {
Expand Down Expand Up @@ -88,6 +102,7 @@ const RewardPanel = ({
}

const challenge = userChallenges[id]

const shouldShowCompleted =
challenge?.state === 'completed' || challenge?.state === 'disbursed'
const hasDisbursed = challenge?.state === 'disbursed'
Expand Down Expand Up @@ -128,13 +143,9 @@ const RewardPanel = ({
)
: ''
}
const buttonMessage = needsDisbursement
? messages.claimReward
: hasDisbursed
? messages.viewDetails
: panelButtonText
const buttonMessage = hasDisbursed ? messages.viewDetails : panelButtonText

const buttonVariant = needsDisbursement ? 'primary' : 'secondary'
const buttonVariant = 'secondary'

return (
<div
Expand All @@ -144,7 +155,7 @@ const RewardPanel = ({
onClick={openRewardModal}
>
<div className={wm(styles.rewardPanelTop)}>
<div className={wm(styles.pillContainer)}>
<div className={wm(styles.rewardPillContainer)}>
{needsDisbursement ? (
<span className={styles.pillMessage}>{messages.readyToClaim}</span>
) : showNewChallengePill ? (
Expand Down Expand Up @@ -193,6 +204,60 @@ const RewardPanel = ({
)
}

const ClaimAllPanel = () => {
const wm = useWithMobileStyle(styles.mobile)
const optimisticUserChallenges = useSelector(getOptimisticUserChallenges)

const totalClaimableAmount = Object.values(optimisticUserChallenges).reduce(
(sum, challenge) => sum + challenge.claimableAmount,
0
)
const pendingChallengeSchedule = usePendingChallengeSchedule()
const [, setClaimAllRewardsVisibility] = useModalState('ClaimAllRewards')
const onClickClaimAllRewards = () => {
setClaimAllRewardsVisibility(true)
}
const pendingAmount = pendingChallengeSchedule.claimableAmount

return (
<Paper
shadow='flat'
border='strong'
p='xl'
alignItems='center'
alignSelf='stretch'
justifyContent='space-between'
m='s'
>
<Flex gap='l' alignItems='center'>
<IconTokenGold
height={48}
width={48}
aria-label={messages.goldAudioToken}
/>
<Flex direction='column'>
<Flex>
<Text color='accent' size='m' variant='heading'>
{messages.totalReadyToClaim}
</Text>
<div className={wm(styles.pendingPillContainer)}>
<span className={styles.pillMessage}>
{pendingAmount} {messages.pending}
</span>
</div>
</Flex>
<Text variant='body' textAlign='left'>
{totalClaimableAmount} {messages.availableNow}
</Text>
</Flex>
</Flex>
<Button onClick={onClickClaimAllRewards} iconRight={IconArrow}>
{messages.claimAllRewards}
</Button>
</Paper>
)
}

type RewardsTileProps = {
className?: string
}
Expand Down Expand Up @@ -233,6 +298,9 @@ const RewardsTile = ({ className }: RewardsTileProps) => {
const optimisticUserChallenges = useSelector(getOptimisticUserChallenges)
const [haveChallengesLoaded, setHaveChallengesLoaded] = useState(false)
const isAudioMatchingChallengesEnabled = useIsAudioMatchingChallengesEnabled()
const { isEnabled: isRewardsCooldownEnabled } = useFeatureFlag(
FeatureFlags.REWARDS_COOLDOWN
)

// The referred challenge only needs a tile if the user was referred
const hideReferredTile = !userChallenges.referred?.is_complete
Expand Down Expand Up @@ -281,13 +349,15 @@ const RewardsTile = ({ className }: RewardsTileProps) => {
<div className={wm(styles.subtitle)}>
<span>{messages.description1}</span>
</div>
<div className={styles.rewardsContainer}>
{userChallengesLoading && !haveChallengesLoaded ? (
<LoadingSpinner className={wm(styles.loadingRewardsTile)} />
) : (
rewardsTiles
)}
</div>
{userChallengesLoading && !haveChallengesLoaded ? (
<LoadingSpinner className={wm(styles.loadingRewardsTile)} />
) : (
<>
{isRewardsCooldownEnabled ? <ClaimAllPanel></ClaimAllPanel> : null}
<Divider orientation='horizontal' className={wm(styles.divider)} />
<div className={styles.rewardsContainer}>{rewardsTiles}</div>
</>
)}
</Tile>
)
}
Expand Down
13 changes: 12 additions & 1 deletion packages/web/src/pages/audio-rewards-page/RewardsTile.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,18 @@
background: var(--harmony-n-25);
}

.pillContainer {
.divider {
align-self: stretch;
margin: 24px 8px;
border: 1px solid var(--harmony-n-150);
}

.pendingPillContainer {
height: 24px;
margin: 8px;
}

.rewardPillContainer {
width: 100%;
height: 24px;
margin: 8px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
formatAudioMatchingChallengesForCooldownSchedule,
usePendingChallengeSchedule
} from '@audius/common/hooks'
import { challengesSelectors } from '@audius/common/store'
import { formatNumberCommas } from '@audius/common/utils'
import { Button, Flex, ModalContent, Text } from '@audius/harmony'
import { useSelector } from 'react-redux'

import { useModalState } from 'common/hooks/useModalState'
import { SummaryTable } from 'components/summary-table'
import { useWithMobileStyle } from 'hooks/useWithMobileStyle'

import ModalDrawer from '../ModalDrawer'

import styles from './styles.module.css'

const { getOptimisticUserChallenges } = challengesSelectors

const messages = {
upcomingRewards: 'Upcoming Rewards',
claimAudio: (amount: string) => `Claim ${amount} $AUDIO`,
readyToClaim: 'Ready to Claim',
rewards: 'Rewards',
audio: '$AUDIO',
description: 'You can check and claim all your upcoming rewards here.',
done: 'Done'
}

export const ClaimAllRewardsModal = () => {
const [isOpen, setOpen] = useModalState('ClaimAllRewards')
const [isHCaptchaModalOpen] = useModalState('HCaptcha')
const wm = useWithMobileStyle(styles.mobile)
const userChallenges = useSelector(getOptimisticUserChallenges)

const undisbursedChallenges = usePendingChallengeSchedule().cooldownChallenges
// TODO merge conflicting dates
const totalClaimableAmount = Object.values(userChallenges).reduce(
(sum, challenge) => sum + challenge.claimableAmount,
0
)
const claimInProgress = false
const onClaimRewardClicked = () => {}
return (
<ModalDrawer
title={messages.rewards}
showTitleHeader
isOpen={isOpen}
onClose={() => setOpen(false)}
isFullscreen={true}
useGradientTitle={false}
titleClassName={wm(styles.title)}
headerContainerClassName={styles.header}
showDismissButton={!isHCaptchaModalOpen}
dismissOnClickOutside={!isHCaptchaModalOpen}
>
<ModalContent>
<Flex direction='column' gap='2xl' mt='s'>
<Text variant='body' textAlign='left'>
{messages.description}
</Text>
<SummaryTable
title={messages.upcomingRewards}
items={formatAudioMatchingChallengesForCooldownSchedule(
undisbursedChallenges
)}
summaryItem={{
id: messages.readyToClaim,
label: messages.readyToClaim,
value: totalClaimableAmount
}}
secondaryTitle={messages.audio}
summaryLabelColor='accent'
summaryValueColor='default'
/>
{totalClaimableAmount > 0 ? (
<Button
fullWidth
isLoading={claimInProgress}
onClick={onClaimRewardClicked}
>
{messages.claimAudio(formatNumberCommas(totalClaimableAmount))}
</Button>
) : (
<Button variant='secondary' fullWidth>
{messages.done}
</Button>
)}
</Flex>
</ModalContent>
</ModalDrawer>
)
}
2 changes: 2 additions & 0 deletions packages/web/src/pages/modals/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { CoinflowWithdrawModal } from 'components/withdraw-usdc-modal/components
import { useIsMobile } from 'hooks/useIsMobile'
import AudioBreakdownModal from 'pages/audio-rewards-page/components/modals/AudioBreakdownModal'
import { ChallengeRewardsModal } from 'pages/audio-rewards-page/components/modals/ChallengeRewardsModal'
import { ClaimAllRewardsModal } from 'pages/audio-rewards-page/components/modals/ChallengeRewardsModal/ClaimAllRewardsModal'
import TopAPIModal from 'pages/audio-rewards-page/components/modals/TopAPI'
import TransferAudioMobileDrawer from 'pages/audio-rewards-page/components/modals/TransferAudioMobileDrawer'
import { VipDiscordModal } from 'pages/audio-rewards-page/components/modals/VipDiscordModal'
Expand Down Expand Up @@ -96,6 +97,7 @@ const commonModalsMap: { [Modal in ModalTypes]?: ComponentType } = {
APIRewardsExplainer: TopAPIModal,
TrendingRewardsExplainer: TrendingRewardsModal,
ChallengeRewardsExplainer: ChallengeRewardsModal,
ClaimAllRewards: ClaimAllRewardsModal,
TransferAudioMobileWarning: TransferAudioMobileDrawer,
BrowserPushPermissionConfirmation: BrowserPushConfirmationModal,
AiAttributionSettings: AiAttributionSettingsModal,
Expand Down

0 comments on commit b16bad3

Please sign in to comment.