diff --git a/package.json b/package.json index cc0966b210..c7cae8e66a 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@hyperplay/chains": "^0.3.0", "@hyperplay/check-disk-space": "^3.5.2", - "@hyperplay/quests-ui": "^0.0.28", + "@hyperplay/quests-ui": "^0.0.38", "@hyperplay/ui": "^1.8.9", "@hyperplay/utils": "^0.3.4", "@mantine/carousel": "^7.12.0", @@ -401,7 +401,8 @@ "classic-level": false, "i18next-parser>esbuild": false, "wagmi>@wagmi/connectors>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": true, - "@hyperplay/providers>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": true + "@hyperplay/providers>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": true, + "@hyperplay/overlay>electron": false } } } diff --git a/src/common/types.ts b/src/common/types.ts index 4fc2acff56..cb534ef9c8 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -12,6 +12,7 @@ import { import { Channel, ContractMetadata } from '@valist/sdk/dist/typesApi' import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { DropdownItemType } from '@hyperplay/ui' +export type { Quest } from '@hyperplay/utils' export type { Listing as HyperPlayRelease, @@ -932,30 +933,6 @@ export interface Reward { numClaimsLeft: string } -export interface Quest { - id: number - project_id: string - name: string - type: 'REPUTATIONAL-AIRDROP' | 'PLAYSTREAK' - status: string - description: string - rewards?: Reward[] - /* eslint-disable-next-line */ - deposit_contracts: any[] - eligibility?: { - completion_threshold?: number - steam_games: { id: string }[] - play_streak: { - required_playstreak_in_days: number - minimum_session_time_in_seconds: number - } - } - quest_external_game: null | { - runner: Runner - store_redirect_url: string - } -} - export interface RewardClaimSignature { signature: `0x${string}` nonce: string diff --git a/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.module.scss b/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.module.scss index 47e564019e..baad1baf44 100644 --- a/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.module.scss +++ b/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.module.scss @@ -1,5 +1,5 @@ .questLog { margin: 0 var(--space-sm) 0 0; - min-width: 280px; + min-width: 320px; max-width: 400px; } diff --git a/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.tsx b/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.tsx index 22f59b3b64..21f9e16577 100644 --- a/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.tsx +++ b/src/frontend/components/UI/QuestsViewer/components/QuestLogWrapper/index.tsx @@ -7,10 +7,10 @@ import { } from '@hyperplay/ui' import styles from './index.module.scss' import { useTranslation } from 'react-i18next' -import { Quest } from 'common/types' import useGetG7UserCredits from 'frontend/hooks/useGetG7UserCredits' import useGetPointsBalancesForProject from 'frontend/hooks/useGetPointsBalances' import useGetQuests from 'frontend/hooks/useGetQuests' +import { useGetQuestStates } from 'frontend/hooks/useGetQuestStates' export interface QuestLogWrapperProps { questsResults: ReturnType @@ -31,6 +31,27 @@ export function QuestLogWrapper({ const pointsBalancesQuery = useGetPointsBalancesForProject(appName) const pointsBalances = pointsBalancesQuery?.data?.data const { t } = useTranslation() + const { questIdToQuestStateMap, isPending: isGetQuestStatesPending } = + useGetQuestStates({ + quests + }) + + const questsUi = + quests?.map((quest) => { + const questUi_i: QuestLogInfo = { + questType: quest.type, + title: quest.name, + state: Object.hasOwn(questIdToQuestStateMap, quest.id) + ? questIdToQuestStateMap[quest.id] + : 'ACTIVE', + onClick: () => { + setSelectedQuestId(quest.id) + }, + selected: selectedQuestId === quest.id, + id: quest.id + } + return questUi_i + }) ?? [] const i18n: QuestLogTranslations = { quests: t('quest.quests', 'Quests'), @@ -68,38 +89,19 @@ export function QuestLogWrapper({ } let questLog = null - if (Array.isArray(quests)) { - const questsUi = quests.map((val: Quest) => { - const questUi_i: QuestLogInfo = { - questType: val.type, - title: val.name, - state: 'ACTIVE', - onClick: () => { - setSelectedQuestId(val.id) - }, - selected: selectedQuestId === val.id, - id: val.id - } - return questUi_i - }) - questLog = ( - - ) - } else if (questsResults?.data.isLoading || questsResults?.data.isFetching) { - questLog = ( - - ) - } + const isLoading = + questsResults?.data.isLoading || + questsResults?.data.isFetching || + isGetQuestStatesPending + questLog = ( + + ) if (!quests?.length) { return null diff --git a/src/frontend/components/UI/QuestsViewer/index.tsx b/src/frontend/components/UI/QuestsViewer/index.tsx index 1a931ec320..a34adefa48 100644 --- a/src/frontend/components/UI/QuestsViewer/index.tsx +++ b/src/frontend/components/UI/QuestsViewer/index.tsx @@ -3,14 +3,13 @@ import styles from './index.module.scss' import { QuestLogWrapper } from './components/QuestLogWrapper' import { Alert } from '@hyperplay/ui' import { useTranslation } from 'react-i18next' -import { QuestDetailsWrapper } from '@hyperplay/quests-ui' +import { QuestDetailsWrapper, useGetUserPlayStreak } from '@hyperplay/quests-ui' import { useFlags } from 'launchdarkly-react-client-sdk' import authState from 'frontend/state/authState' import useAuthSession from 'frontend/hooks/useAuthSession' import '@hyperplay/quests-ui/style.css' import { Reward } from 'common/types' import useGetQuests from 'frontend/hooks/useGetQuests' -import useGetUserPlayStreak from 'frontend/hooks/useGetUserPlayStreak' import { useSyncPlayStreakWithExternalSource } from 'frontend/hooks/useSyncPlayStreakWithExternalSource' import { useAccount } from 'wagmi' @@ -29,7 +28,10 @@ export function QuestsViewer({ projectId: appName }: QuestsViewerProps) { const initialQuestId = quests?.[0]?.id ?? null const visibleQuestId = selectedQuestId ?? initialQuestId const sessionEmail = data?.linkedAccounts.get('email') - const { invalidateQuery } = useGetUserPlayStreak(visibleQuestId) + const { invalidateQuery } = useGetUserPlayStreak( + visibleQuestId, + window.api.getUserPlayStreak + ) const getPendingExternalSync = useCallback(async () => { if (!address || !visibleQuestId || !isSignedIn) return false @@ -123,7 +125,6 @@ export function QuestsViewer({ projectId: appName }: QuestsViewerProps) { getQuestRewardSignature={window.api.getQuestRewardSignature} confirmRewardClaim={window.api.confirmRewardClaim} getExternalTaskCredits={window.api.getExternalTaskCredits} - syncPlaySession={window.api.syncPlaySession} getDepositContracts={window.api.getDepositContracts} openSignInModal={authState.openSignInModal} resyncExternalTask={async (rewardId: string) => { diff --git a/src/frontend/hooks/useGetQuest.ts b/src/frontend/hooks/useGetQuest.ts deleted file mode 100644 index 66e6b3b286..0000000000 --- a/src/frontend/hooks/useGetQuest.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query' - -export default function useGetQuest(questId: number | null) { - const queryClient = useQueryClient() - const queryKey = `getQuest:${questId}` - const query = useQuery({ - queryKey: [queryKey], - queryFn: async () => { - if (questId === null) { - return null - } - const response = await window.api.getQuest(questId) - if (!response) return null - return response - }, - refetchOnWindowFocus: false, - enabled: questId !== null - }) - - return { - data: query, - isLoading: query.isLoading || query.isFetching, - invalidateQuery: async () => - queryClient.invalidateQueries({ queryKey: [queryKey] }) - } -} diff --git a/src/frontend/hooks/useGetQuestStates.ts b/src/frontend/hooks/useGetQuestStates.ts new file mode 100644 index 0000000000..a6698409ec --- /dev/null +++ b/src/frontend/hooks/useGetQuestStates.ts @@ -0,0 +1,74 @@ +import { + getPlaystreakQuestStatus, + getUserPlaystreakQueryOptions, + getQuestQueryOptions +} from '@hyperplay/quests-ui' +import { QuestLogInfo } from '@hyperplay/ui' +import { Quest } from '@hyperplay/utils' +import { useQueries } from '@tanstack/react-query' +import useAuthSession from './useAuthSession' + +export interface UseGetQuestLogInfosProps { + quests?: Quest[] | null +} + +type getQuestQueryOptionsType = ReturnType +type getUserPlaystreakQueryOptionsType = ReturnType< + typeof getUserPlaystreakQueryOptions +> +export function useGetQuestStates({ quests }: UseGetQuestLogInfosProps) { + const { isSignedIn } = useAuthSession() + let getQuestQueries: getQuestQueryOptionsType[] = [] + if (isSignedIn) { + getQuestQueries = + quests?.map((quest) => + getQuestQueryOptions(quest.id, window.api.getQuest) + ) ?? [] + } + const getQuestQuery = useQueries({ + queries: getQuestQueries + }) + + let getUserPlaystreakQueries: getUserPlaystreakQueryOptionsType[] = [] + if (isSignedIn) { + getUserPlaystreakQueries = + quests?.map((quest) => + getUserPlaystreakQueryOptions(quest.id, window.api.getUserPlayStreak) + ) ?? [] + } + const getUserPlaystreakQuery = useQueries({ + queries: getUserPlaystreakQueries + }) + + const questMap: Record = {} + getQuestQuery + .filter((val) => !!val.data) + .forEach((val) => { + if (!val.data) { + return + } + questMap[val.data.id] = val.data + }) + + const questIdToQuestStateMap: Record = {} + + getUserPlaystreakQuery.forEach((val) => { + if (!val.data || !Object.hasOwn(questMap, val.data.questId)) { + return + } + const questId = val.data.questId + const questData = questMap[questId] + return (questIdToQuestStateMap[questId] = getPlaystreakQuestStatus( + questData, + val.data.userPlayStreak + )) + }) + + const allQueries = [...getQuestQuery, ...getUserPlaystreakQuery] + + return { + isPending: allQueries.some((val) => val.status === 'pending'), + isLoading: allQueries.some((val) => val.isLoading || val.isFetching), + questIdToQuestStateMap + } +} diff --git a/src/frontend/hooks/useGetRewards.ts b/src/frontend/hooks/useGetRewards.ts index a90ae99f1c..a026d57396 100644 --- a/src/frontend/hooks/useGetRewards.ts +++ b/src/frontend/hooks/useGetRewards.ts @@ -1,12 +1,12 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import useGetQuest from './useGetQuest' import { getDecimalNumberFromAmount } from '@hyperplay/utils' import { getRewardCategory } from 'frontend/helpers/getRewardCategory' import { useTranslation } from 'react-i18next' import { QuestReward } from '@hyperplay/ui' +import { useGetQuest } from '@hyperplay/quests-ui' export function useGetRewards(questId: number | null) { - const questResult = useGetQuest(questId) + const questResult = useGetQuest(questId, window.api.getQuest) const questMeta = questResult.data.data const queryClient = useQueryClient() diff --git a/src/frontend/hooks/useGetUserPlayStreak.ts b/src/frontend/hooks/useGetUserPlayStreak.ts deleted file mode 100644 index 03d58c480f..0000000000 --- a/src/frontend/hooks/useGetUserPlayStreak.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { UserPlayStreak } from 'common/types' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import useAuthSession from './useAuthSession' - -export default function useGetUserPlayStreak(questId: number | null) { - const { isSignedIn } = useAuthSession() - const queryClient = useQueryClient() - const queryKey = `getUserPlayStreak:${questId}` - const query = useQuery({ - queryKey: [queryKey], - queryFn: async () => { - if (questId === null) { - return null - } - const response = await window.api.getUserPlayStreak(questId) - if (!response) return null - return response - }, - refetchOnWindowFocus: false, - enabled: questId !== null - }) - - return { - enabled: isSignedIn, - refetchInterval: 30_000, // 30 seconds - data: query, - isLoading: query.isLoading || query.isFetching, - invalidateQuery: async () => - queryClient.invalidateQueries({ queryKey: [queryKey] }) - } -} diff --git a/src/frontend/hooks/useSyncInterval.ts b/src/frontend/hooks/useSyncInterval.ts deleted file mode 100644 index 779bae52f6..0000000000 --- a/src/frontend/hooks/useSyncInterval.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { wait } from '@hyperplay/utils' -import { resetSessionStartedTime } from '@hyperplay/quests-ui' -import { useEffect } from 'react' - -export function useSyncPlaySession( - projectId: string, - invalidateQuery: () => Promise -) { - useEffect(() => { - const syncTimer = setInterval(async () => { - await window.api.syncPlaySession(projectId, 'hyperplay') - // allow for some time before read - await wait(5000) - await invalidateQuery() - resetSessionStartedTime() - }, 1000 * 60) - - return () => { - clearInterval(syncTimer) - } - }, [projectId, invalidateQuery]) -} diff --git a/src/frontend/screens/Quests/components/QuestDetailsViewPlay/index.tsx b/src/frontend/screens/Quests/components/QuestDetailsViewPlay/index.tsx index aa2cbecb21..013449943c 100644 --- a/src/frontend/screens/Quests/components/QuestDetailsViewPlay/index.tsx +++ b/src/frontend/screens/Quests/components/QuestDetailsViewPlay/index.tsx @@ -6,13 +6,15 @@ import { QuestDetailsTranslations } from '@hyperplay/ui' import { useTranslation } from 'react-i18next' -import useGetQuest from 'frontend/hooks/useGetQuest' import styles from './index.module.scss' import { useNavigate } from 'react-router-dom' import { fetchEpicListing, getGameInfo } from 'frontend/helpers' import useGetSteamGame from 'frontend/hooks/useGetSteamGame' -import useGetUserPlayStreak from 'frontend/hooks/useGetUserPlayStreak' -import { getPlaystreakArgsFromQuestData } from '@hyperplay/quests-ui' +import { + getPlaystreakArgsFromQuestData, + useGetQuest, + useGetUserPlayStreak +} from '@hyperplay/quests-ui' import { useGetRewards } from 'frontend/hooks/useGetRewards' import { useMutation } from '@tanstack/react-query' import { Runner } from 'common/types' @@ -28,9 +30,12 @@ export const QuestDetailsViewPlayWrapper = observer( const { t } = useTranslation() const [collapseIsOpen, setCollapseIsOpen] = useState(false) - const questResult = useGetQuest(selectedQuestId) + const questResult = useGetQuest(selectedQuestId, window.api.getQuest) const questMeta = questResult.data.data - const questPlayStreakResult = useGetUserPlayStreak(selectedQuestId) + const questPlayStreakResult = useGetUserPlayStreak( + selectedQuestId, + window.api.getUserPlayStreak + ) const questPlayStreakData = questPlayStreakResult.data.data const rewardsQuery = useGetRewards(selectedQuestId) @@ -204,6 +209,9 @@ export const QuestDetailsViewPlayWrapper = observer( ) } + const dateTimeCurrentSessionStartedInMsSinceEpoch = + questPlayStreakResult?.data.dataUpdatedAt ?? Date.now() + return (