From 3baf4fc952ffd201390467cdc5aae870f9cfbff7 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Mon, 6 Nov 2023 23:13:05 -0500 Subject: [PATCH 1/7] [PAY-2030] Use cooldown_days from backend instead of hardcoding on client --- .../src/hooks/purchaseContent/constants.ts | 2 - .../useChallengeCooldownSchedule.ts | 41 ++++++++++--------- .../src/store/pages/audio-rewards/types.ts | 1 + packages/common/src/utils/challenges.ts | 32 ++++++++++++--- .../src/api/v1/challenges.py | 1 + .../src/api/v1/models/challenges.py | 2 + .../src/queries/get_undisbursed_challenges.py | 2 + .../ChallengeRewardsDrawerProvider.tsx | 27 ++++++++---- .../ChallengeRewardsModal.tsx | 17 +------- 9 files changed, 73 insertions(+), 52 deletions(-) diff --git a/packages/common/src/hooks/purchaseContent/constants.ts b/packages/common/src/hooks/purchaseContent/constants.ts index c43748e0fee..421cdbb3401 100644 --- a/packages/common/src/hooks/purchaseContent/constants.ts +++ b/packages/common/src/hooks/purchaseContent/constants.ts @@ -4,5 +4,3 @@ export const AMOUNT_PRESET = 'amountPreset' // Pay between $1 and $100 extra export const minimumPayExtraAmountCents = 100 export const maximumPayExtraAmountCents = 10000 - -export const COOLDOWN_DAYS = 7 diff --git a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts index f7640ef2bba..52526398d56 100644 --- a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts +++ b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts @@ -7,8 +7,7 @@ import { UndisbursedUserChallenge, audioRewardsPageSelectors } from 'store/pages' - -import { COOLDOWN_DAYS } from './constants' +import { isCooldownChallengeClaimable, toLocalTime } from 'utils/challenges' const { getUndisbursedUserChallenges } = audioRewardsPageSelectors @@ -30,40 +29,44 @@ export const useChallengeCooldownSchedule = ( const challenges = useSelector(getUndisbursedUserChallenges) .filter((c) => c.challenge_id === challengeId) .map((c) => ({ ...c, createdAtDate: dayjs.utc(c.created_at) })) - const now = dayjs.utc() // Only challenges past the cooldown period are claimable - const claimableAmount = challenges - .filter((c) => now.diff(c.createdAtDate, 'day') >= COOLDOWN_DAYS) - .reduce((acc, curr) => acc + curr.amount, 0) // Challenges are already ordered by completed_blocknumber ascending. const cooldownChallenges = challenges.filter( - (c) => now.diff(c.createdAtDate, 'day') < COOLDOWN_DAYS + (c) => !isCooldownChallengeClaimable(c) ) - return { claimableAmount, cooldownChallenges } + const claimableAmount = challenges + .filter(isCooldownChallengeClaimable) + .reduce((acc, curr) => acc + curr.amount, 0) + return { cooldownChallenges, claimableAmount } } -const getAudioMatchingCooldownLabel = (now: Dayjs, created_at: Dayjs) => { - const diff = now.diff(created_at, 'day') - if (diff === COOLDOWN_DAYS) { +const getAudioMatchingCooldownLabel = ( + challenge: UndisbursedUserChallenge, + now: Dayjs +) => { + const createdAt = toLocalTime(challenge.created_at) + const cooldownDays = challenge.cooldown_days ?? 0 + const diff = now.diff(createdAt, 'day') + if (diff === cooldownDays) { return messages.laterToday - } else if (diff === COOLDOWN_DAYS - 1) { + } else if (diff === cooldownDays - 1) { return messages.tomorrow } - return created_at.local().add(COOLDOWN_DAYS, 'day').format('ddd (M/D)') + return createdAt.add(cooldownDays, 'day').format('ddd (M/D)') } -const formatAudioMatchingChallengeCooldownSchedule = ( +const formatAudioMatchingChallengesForCooldownSchedule = ( challenges: UndisbursedUserChallenge[] ) => { - const now = dayjs.utc().endOf('day') + const now = dayjs().endOf('day') const cooldownChallenges = new Array(7) challenges.forEach((c) => { - const createdAtUTC = dayjs.utc(c.created_at) - const diff = now.diff(createdAtUTC, 'day') + const createdAt = toLocalTime(c.created_at) + const diff = now.diff(createdAt, 'day') cooldownChallenges[diff] = { ...cooldownChallenges[diff], id: c.specifier, - label: getAudioMatchingCooldownLabel(now, createdAtUTC), + label: getAudioMatchingCooldownLabel(c, now), value: (cooldownChallenges[diff]?.value ?? 0) + c.amount } }) @@ -87,7 +90,7 @@ export const useAudioMatchingChallengeCooldownSchedule = ( return { claimableAmount, cooldownChallenges: - formatAudioMatchingChallengeCooldownSchedule(cooldownChallenges), + formatAudioMatchingChallengesForCooldownSchedule(cooldownChallenges), cooldownChallengesSummary: claimableAmount > 0 ? getAudioMatchingChallengeCooldownSummary(claimableAmount) diff --git a/packages/common/src/store/pages/audio-rewards/types.ts b/packages/common/src/store/pages/audio-rewards/types.ts index 52cc6e8d80f..7383df602fe 100644 --- a/packages/common/src/store/pages/audio-rewards/types.ts +++ b/packages/common/src/store/pages/audio-rewards/types.ts @@ -29,6 +29,7 @@ export type UndisbursedUserChallenge = Pick< handle: string wallet: string created_at: string + cooldown_days?: number } export enum HCaptchaStatus { diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index 3736e57b1a2..c6703d049af 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -6,7 +6,8 @@ import { ChallengeRewardID, UserChallenge, OptimisticUserChallenge, - ChallengeName + ChallengeName, + SpecifierWithAmount } from '../models' import { formatNumberCommas } from './formatUtil' @@ -248,14 +249,33 @@ export const isAudioMatchingChallenge = ( ) } -// TODO: currently only $AUDIO matching challenges have cooldown -// so this works, but really we should check if `cooldown_period` exists on the -// challenge instead of using `!isAudioMatchingChallenge`. PAY-2030 +/** Returns true if the challenge is not a cooldown challenge by checking + * whether it has `cooldown_days` defined and whether the challenge has been + * created for more than `cooldown_days` days. + */ export const isCooldownChallengeClaimable = ( challenge: UndisbursedUserChallenge ) => { return ( - !isAudioMatchingChallenge(challenge.challenge_id) || - dayjs.utc().diff(dayjs.utc(challenge.created_at), 'day') >= 7 + challenge.cooldown_days === undefined || + dayjs.utc().diff(dayjs.utc(challenge.created_at), 'day') >= + challenge.cooldown_days ) } + +/* Filter for only claimable challenges */ +export const getClaimableChallengeSpecifiers = ( + specifiers: SpecifierWithAmount[], + undisbursedUserChallenges: UndisbursedUserChallenge[] +) => { + return specifiers.filter((s) => { + const challenge = undisbursedUserChallenges.filter( + (c) => c.specifier === s.specifier + )[0] // specifiers are unique + return isCooldownChallengeClaimable(challenge) + }) +} + +export const toLocalTime = (date: string) => { + return dayjs.utc(date).local() +} diff --git a/packages/discovery-provider/src/api/v1/challenges.py b/packages/discovery-provider/src/api/v1/challenges.py index 28b40318410..1a3cc42c4db 100644 --- a/packages/discovery-provider/src/api/v1/challenges.py +++ b/packages/discovery-provider/src/api/v1/challenges.py @@ -247,6 +247,7 @@ def get(self, challenge_id: str): "starting_block": challenge.starting_block, "weekly_pool": challenge.weekly_pool, "weekly_pool_remaining": weekly_pool_remaining, + "cooldown_days": challenge.cooldown_days, } if ( weekly_pool_min_amount diff --git a/packages/discovery-provider/src/api/v1/models/challenges.py b/packages/discovery-provider/src/api/v1/models/challenges.py index c448fa7e6da..3b0c31a21d7 100644 --- a/packages/discovery-provider/src/api/v1/models/challenges.py +++ b/packages/discovery-provider/src/api/v1/models/challenges.py @@ -21,6 +21,7 @@ "handle": fields.String(required=True), "wallet": fields.String(required=True), "created_at": fields.String(required=True), + "cooldown_days": fields.Integer(required=False), }, ) @@ -43,5 +44,6 @@ "starting_block": fields.Integer(required=False), "weekly_pool": fields.String(required=False), "weekly_pool_remaining": fields.String(required=False), + "cooldown_days": fields.Integer(required=False), }, ) diff --git a/packages/discovery-provider/src/queries/get_undisbursed_challenges.py b/packages/discovery-provider/src/queries/get_undisbursed_challenges.py index 4072eb05c64..af9908c1538 100644 --- a/packages/discovery-provider/src/queries/get_undisbursed_challenges.py +++ b/packages/discovery-provider/src/queries/get_undisbursed_challenges.py @@ -17,6 +17,7 @@ class UndisbursedChallengeResponse(TypedDict): handle: str wallet: str created_at: str + cooldown_days: Optional[int] def to_challenge_response( @@ -34,6 +35,7 @@ def to_challenge_response( "handle": handle, "wallet": wallet, "created_at": str(user_challenge.created_at), + "cooldown_days": challenge.cooldown_days, } diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx index ed17cbd3a9a..78ddc8459a8 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx @@ -8,7 +8,8 @@ import { audioRewardsPageActions, ClaimStatus, audioRewardsPageSelectors, - isAudioMatchingChallenge + isAudioMatchingChallenge, + getClaimableChallengeSpecifiers } from '@audius/common' import { useDispatch, useSelector } from 'react-redux' @@ -25,8 +26,12 @@ import { AudioMatchingChallengeDrawerContent } from './AudioMatchingChallengeDra import { ChallengeRewardsDrawerContent } from './ChallengeRewardsDrawerContent' import { ProfileCompletionChecks } from './ProfileCompletionChecks' import { ReferralRewardContents } from './ReferralRewardContents' -const { getChallengeRewardsModalType, getClaimStatus, getAAOErrorCode } = - audioRewardsPageSelectors +const { + getChallengeRewardsModalType, + getClaimStatus, + getAAOErrorCode, + getUndisbursedUserChallenges +} = audioRewardsPageSelectors const { claimChallengeReward, resetAndCancelClaimReward } = audioRewardsPageActions const { getOptimisticUserChallenges } = challengesSelectors @@ -51,6 +56,7 @@ export const ChallengeRewardsDrawerProvider = () => { const userChallenges = useSelector((state: CommonState) => getOptimisticUserChallenges(state, true) ) + const undisbursedUserChallenges = useSelector(getUndisbursedUserChallenges) const handleClose = useCallback(() => { dispatch(resetAndCancelClaimReward()) @@ -111,12 +117,15 @@ export const ChallengeRewardsDrawerProvider = () => { claimChallengeReward({ claim: { challengeId: modalType, - specifiers: [ - { - specifier: challenge.specifier, - amount: challenge.amount - } - ], + specifiers: + challenge.challenge_type === 'aggregate' + ? getClaimableChallengeSpecifiers( + challenge.undisbursedSpecifiers, + undisbursedUserChallenges + ) + : [ + { specifier: challenge.specifier, amount: challenge.amount } + ], amount: challenge?.claimableAmount ?? 0 }, retryOnFailure: true 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 791f8e6ef81..a7a1879cac0 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 @@ -13,9 +13,7 @@ import { musicConfettiActions, challengeRewardsConfig, isAudioMatchingChallenge, - isCooldownChallengeClaimable, - SpecifierWithAmount, - UndisbursedUserChallenge + getClaimableChallengeSpecifiers } from '@audius/common' import { Button, @@ -219,19 +217,6 @@ const getErrorMessage = (aaoErrorCode?: number) => { return <>{messages.claimError} } -/* Filter for only claimable challenges */ -const getClaimableChallengeSpecifiers = ( - specifiers: SpecifierWithAmount[], - undisbursedUserChallenges: UndisbursedUserChallenge[] -) => { - return specifiers.filter((s) => { - const challenge = undisbursedUserChallenges.filter( - (c) => c.specifier === s.specifier - )[0] // specifiers are unique - return isCooldownChallengeClaimable(challenge) - }) -} - type BodyProps = { dismissModal: () => void } From abf4a5847bc57d6df9db201b9bac530630daf8fb Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 12:56:13 -0500 Subject: [PATCH 2/7] remove mobile fix --- .../ChallengeRewardsDrawerProvider.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx index 78ddc8459a8..2a1429a87ac 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx @@ -117,15 +117,12 @@ export const ChallengeRewardsDrawerProvider = () => { claimChallengeReward({ claim: { challengeId: modalType, - specifiers: - challenge.challenge_type === 'aggregate' - ? getClaimableChallengeSpecifiers( - challenge.undisbursedSpecifiers, - undisbursedUserChallenges - ) - : [ - { specifier: challenge.specifier, amount: challenge.amount } - ], + specifiers: [ + { + specifier: challenge.specifier, + amount: challenge.amount + } + ], amount: challenge?.claimableAmount ?? 0 }, retryOnFailure: true From 3a536b94f588be5af87e5399f7a40323db09c5e3 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 12:57:19 -0500 Subject: [PATCH 3/7] remove imports --- .../ChallengeRewardsDrawerProvider.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx index 2a1429a87ac..ed17cbd3a9a 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ChallengeRewardsDrawerProvider.tsx @@ -8,8 +8,7 @@ import { audioRewardsPageActions, ClaimStatus, audioRewardsPageSelectors, - isAudioMatchingChallenge, - getClaimableChallengeSpecifiers + isAudioMatchingChallenge } from '@audius/common' import { useDispatch, useSelector } from 'react-redux' @@ -26,12 +25,8 @@ import { AudioMatchingChallengeDrawerContent } from './AudioMatchingChallengeDra import { ChallengeRewardsDrawerContent } from './ChallengeRewardsDrawerContent' import { ProfileCompletionChecks } from './ProfileCompletionChecks' import { ReferralRewardContents } from './ReferralRewardContents' -const { - getChallengeRewardsModalType, - getClaimStatus, - getAAOErrorCode, - getUndisbursedUserChallenges -} = audioRewardsPageSelectors +const { getChallengeRewardsModalType, getClaimStatus, getAAOErrorCode } = + audioRewardsPageSelectors const { claimChallengeReward, resetAndCancelClaimReward } = audioRewardsPageActions const { getOptimisticUserChallenges } = challengesSelectors @@ -56,7 +51,6 @@ export const ChallengeRewardsDrawerProvider = () => { const userChallenges = useSelector((state: CommonState) => getOptimisticUserChallenges(state, true) ) - const undisbursedUserChallenges = useSelector(getUndisbursedUserChallenges) const handleClose = useCallback(() => { dispatch(resetAndCancelClaimReward()) From 8a0e0b2943485a7346fe7943b83c59f88ef7dfad Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 15:27:52 -0500 Subject: [PATCH 4/7] fix tests --- .../integration_tests/queries/test_undisbursed_challenges.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py b/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py index 3935e9c59e4..2409554a594 100644 --- a/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py +++ b/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py @@ -153,6 +153,7 @@ def test_undisbursed_challenges(app): "handle": "TestHandle6", "wallet": "0x6", "created_at": "2023-10-16 17:51:31.105065+00:00", + "cooldown_days": None, }, { "challenge_id": "test_challenge_2", @@ -163,6 +164,7 @@ def test_undisbursed_challenges(app): "handle": "TestHandle4", "wallet": "0x4", "created_at": "2023-10-16 17:51:31.105065+00:00", + "cooldown_days": None, }, { "challenge_id": "test_challenge_2", @@ -173,6 +175,7 @@ def test_undisbursed_challenges(app): "handle": "TestHandle5", "wallet": "0x5", "created_at": "2023-10-16 17:51:31.105065+00:00", + "cooldown_days": None, }, ] assert expected == undisbursed @@ -193,6 +196,7 @@ def test_undisbursed_challenges(app): "handle": "TestHandle6", "wallet": "0x6", "created_at": "2023-10-16 17:51:31.105065+00:00", + "cooldown_days": None, }, ] assert expected == undisbursed From 690366ef3080c55fbccbf0d5f677f9df01e713f5 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 15:31:04 -0500 Subject: [PATCH 5/7] avoid [0] --- .../hooks/purchaseContent/useChallengeCooldownSchedule.ts | 3 ++- packages/common/src/utils/challenges.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts index 52526398d56..7309a04a62f 100644 --- a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts +++ b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts @@ -58,8 +58,9 @@ const getAudioMatchingCooldownLabel = ( const formatAudioMatchingChallengesForCooldownSchedule = ( challenges: UndisbursedUserChallenge[] ) => { + if (challenges.length === 0) return [] const now = dayjs().endOf('day') - const cooldownChallenges = new Array(7) + const cooldownChallenges = new Array(challenges[0].cooldown_days) challenges.forEach((c) => { const createdAt = toLocalTime(c.created_at) const diff = now.diff(createdAt, 'day') diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index c6703d049af..35bb4c6cebc 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -271,8 +271,10 @@ export const getClaimableChallengeSpecifiers = ( return specifiers.filter((s) => { const challenge = undisbursedUserChallenges.filter( (c) => c.specifier === s.specifier - )[0] // specifiers are unique - return isCooldownChallengeClaimable(challenge) + ) + if (challenge.length === 0) return false + // specifiers are unique + return isCooldownChallengeClaimable(challenge[0]) }) } From 41efee7a455b1b49c1bf9270d5374edb63d22db1 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 18:59:50 -0500 Subject: [PATCH 6/7] move toLocalTime --- .../hooks/purchaseContent/useChallengeCooldownSchedule.ts | 7 ++++--- packages/common/src/utils/challenges.ts | 6 ------ packages/common/src/utils/timeUtil.ts | 6 ++++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts index 7309a04a62f..046ad8d9c02 100644 --- a/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts +++ b/packages/common/src/hooks/purchaseContent/useChallengeCooldownSchedule.ts @@ -7,7 +7,8 @@ import { UndisbursedUserChallenge, audioRewardsPageSelectors } from 'store/pages' -import { isCooldownChallengeClaimable, toLocalTime } from 'utils/challenges' +import { isCooldownChallengeClaimable } from 'utils/challenges' +import { utcToLocalTime } from 'utils/timeUtil' const { getUndisbursedUserChallenges } = audioRewardsPageSelectors @@ -44,7 +45,7 @@ const getAudioMatchingCooldownLabel = ( challenge: UndisbursedUserChallenge, now: Dayjs ) => { - const createdAt = toLocalTime(challenge.created_at) + const createdAt = utcToLocalTime(challenge.created_at) const cooldownDays = challenge.cooldown_days ?? 0 const diff = now.diff(createdAt, 'day') if (diff === cooldownDays) { @@ -62,7 +63,7 @@ const formatAudioMatchingChallengesForCooldownSchedule = ( const now = dayjs().endOf('day') const cooldownChallenges = new Array(challenges[0].cooldown_days) challenges.forEach((c) => { - const createdAt = toLocalTime(c.created_at) + const createdAt = utcToLocalTime(c.created_at) const diff = now.diff(createdAt, 'day') cooldownChallenges[diff] = { ...cooldownChallenges[diff], diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index 35bb4c6cebc..987efefe9c1 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -1,5 +1,3 @@ -import dayjs from 'dayjs' - import { UndisbursedUserChallenge } from 'store/pages' import { @@ -277,7 +275,3 @@ export const getClaimableChallengeSpecifiers = ( return isCooldownChallengeClaimable(challenge[0]) }) } - -export const toLocalTime = (date: string) => { - return dayjs.utc(date).local() -} diff --git a/packages/common/src/utils/timeUtil.ts b/packages/common/src/utils/timeUtil.ts index 507b1575619..613f79640d3 100644 --- a/packages/common/src/utils/timeUtil.ts +++ b/packages/common/src/utils/timeUtil.ts @@ -1,6 +1,8 @@ import type { MomentInput } from 'moment' import moment from 'moment' +import dayjs from 'dayjs' + const SECONDS_PER_MINUTE = 60 const MINUTES_PER_HOUR = 60 const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR @@ -45,3 +47,7 @@ export const formatDate = (date: MomentInput, format?: string): string => { export const formatDateWithTimezoneOffset = (date: MomentInput): string => { return moment(date).add(moment().utcOffset(), 'm').format('MM/DD/YY') } + +export const utcToLocalTime = (date: string) => { + return dayjs.utc(date).local() +} From a5b21d696aa97fc724756705e4518a1ed6ba12b8 Mon Sep 17 00:00:00 2001 From: Dharit Tantiviramanond Date: Tue, 7 Nov 2023 20:51:37 -0500 Subject: [PATCH 7/7] lint --- packages/common/src/utils/challenges.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/common/src/utils/challenges.ts b/packages/common/src/utils/challenges.ts index 987efefe9c1..8c4b972f048 100644 --- a/packages/common/src/utils/challenges.ts +++ b/packages/common/src/utils/challenges.ts @@ -1,3 +1,5 @@ +import dayjs from 'dayjs' + import { UndisbursedUserChallenge } from 'store/pages' import {