From 72d45aff1b72dd5bc0b3f4bc5651ece83c70505b Mon Sep 17 00:00:00 2001 From: lw Date: Wed, 8 May 2024 19:34:28 +0700 Subject: [PATCH 01/13] Update task category for mission center --- .../Popup/Home/Mission/TaskCategoryList.tsx | 85 ++++++++++++ .../src/Popup/Home/Mission/TaskItem.tsx | 88 ++++++------ .../src/Popup/Home/Mission/TaskList.tsx | 96 +++++++++++++ .../src/Popup/Home/Mission/index.tsx | 131 +++++++++++++----- .../Layout/parts/SelectAccount/index.tsx | 6 +- .../src/connector/booka/sdk.ts | 38 ++++- .../src/connector/booka/types.ts | 38 +++-- .../src/contexts/DataContext.tsx | 4 +- 8 files changed, 387 insertions(+), 99 deletions(-) create mode 100644 packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx create mode 100644 packages/extension-koni-ui/src/Popup/Home/Mission/TaskList.tsx diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx new file mode 100644 index 00000000000..08649539771 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx @@ -0,0 +1,85 @@ +// Copyright 2019-2022 @subwallet/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { TaskCategory, TaskCategoryInfo } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { formatInteger } from '@subwallet/extension-koni-ui/utils'; +import { Image, Typography } from '@subwallet/react-ui'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + taskCategoryList: TaskCategory[]; + taskCategoryInfoMap: Record; + onClickCategoryItem: (categoryId: number) => void; +}; + +const Component = ({ className, onClickCategoryItem, taskCategoryInfoMap, taskCategoryList }: Props): React.ReactElement => { + const { t } = useTranslation(); + + const filteredTaskCategoryList = useMemo(() => { + return taskCategoryList.filter((tc) => { + return taskCategoryInfoMap[tc.id] && taskCategoryInfoMap[tc.id].tasks.length; + }); + }, [taskCategoryInfoMap, taskCategoryList]); + + const onClickItem = useCallback((categoryId: number) => { + return () => { + onClickCategoryItem(categoryId); + }; + }, [onClickCategoryItem]); + + return ( +
+ + {t('Categories')} + + + { + filteredTaskCategoryList.map((tc) => ( +
+ +
+
{tc.name}
+ +
+ Min point can earn: {formatInteger(taskCategoryInfoMap[tc.id]?.minPoint || 0)} +
+
+
+ )) + } +
+ ); +}; + +export const TaskCategoryList = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { + return { + '.task-category-banner': { + marginRight: token.marginSM + }, + + '.task-category-item': { + display: 'flex', + backgroundColor: token.colorBgSecondary, + minHeight: 50, + borderRadius: token.borderRadiusLG, + padding: token.padding, + cursor: 'pointer', + alignItems: 'center' + }, + + '.task-category-item + .task-category-item': { + marginTop: token.marginXS + } + }; +}); diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskItem.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskItem.tsx index c27808af43b..16e14c76506 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskItem.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskItem.tsx @@ -26,7 +26,6 @@ const _TaskItem = ({ className, task }: Props): React.ReactElement => { useSetCurrentPage('/home/mission'); const [taskLoading, setTaskLoading] = useState(false); const { t } = useTranslation(); - const [disabled, setDisabled] = useState(false); const completed = !!task.completedAt; const finishTask = useCallback(() => { @@ -39,56 +38,42 @@ const _TaskItem = ({ className, task }: Props): React.ReactElement => { setTaskLoading(false); }) .catch(console.error); + setTimeout(() => { - telegramConnector.openLink(task.url); + task.url && telegramConnector.openLink(task.url); }, 100); }, [task.id, task.url]); - const CountDownElement = useCallback(() => { - if (completed) { - return <>; - } - + const { endTime, + isDisabled, + isEnd, isInTimeRange, + isNotStarted, + startTime } = (() => { const now = Date.now(); - if (task.startTime) { - const startTime = new Date(task.startTime).getTime(); - - if (startTime > now) { - setDisabled(true); - - return ; - } - } - - if (task.endTime) { - const endTime = new Date(task.endTime).getTime(); - - if (endTime > now) { - return ; - } else { - setDisabled(true); - - return {t('Ended')}; - } - } - - return <>; - }, [completed, t, task.endTime, task.startTime]); + const startTime = task.startTime ? new Date(task.startTime).getTime() : undefined; + const endTime = task.endTime ? new Date(task.endTime).getTime() : undefined; + const isNotStarted = !completed && !!startTime && startTime > now; + const isInTimeRange = !completed && !!endTime && endTime > now; + const isEnd = !completed && !!endTime && endTime <= now; + + return { + startTime, + endTime, + isNotStarted, + isInTimeRange, + isEnd, + isDisabled: isNotStarted || isEnd + }; + })(); return
@@ -100,13 +85,32 @@ const _TaskItem = ({ className, task }: Props): React.ReactElement => { className={'__sub-title'} size={'sm'} > - - + + + { + isNotStarted && !!startTime && ( + + ) + } + { + isInTimeRange && !!endTime && ( + + ) + } + { + isEnd && ({t('Ended')}) + }
{!completed &&
+ + {sortedTaskList.map((task) => ( + + ))} + + ); +}; + +export const TaskList = styled(Component)(({ theme: { extendToken, token } }: ThemeProps) => { + return { + '.__list-header': { + display: 'flex', + alignItems: 'center', + marginBottom: token.marginXS, + + '.ant-typography': { + marginBottom: 0 + } + }, + + '.task-list': { + padding: token.padding, + + '.account-info': { + marginBottom: token.marginSM + } + } + }; +}); diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx index f24a1d8d2cf..30d9b1857d1 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx @@ -3,74 +3,129 @@ import GameAccount from '@subwallet/extension-koni-ui/components/Games/GameAccount'; import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; -import { Task } from '@subwallet/extension-koni-ui/connector/booka/types'; -import { useSetCurrentPage, useTranslation } from '@subwallet/extension-koni-ui/hooks'; -import TaskItem from '@subwallet/extension-koni-ui/Popup/Home/Mission/TaskItem'; +import { Task, TaskCategory, TaskCategoryInfo } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { useSetCurrentPage } from '@subwallet/extension-koni-ui/hooks'; +import { TaskList } from '@subwallet/extension-koni-ui/Popup/Home/Mission/TaskList'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { Typography } from '@subwallet/react-ui'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { TaskCategoryList } from './TaskCategoryList'; + type Props = ThemeProps; const apiSDK = BookaSdk.instance; +enum ViewMode { + CATEGORY_LIST = 'category_list', + TASK_LIST = 'task_list', +} + +function getTaskCategoryInfoMap (tasks: Task[]): Record { + const result: Record = {}; + const now = Date.now(); + + tasks.forEach((t) => { + if (!t.categoryId) { + return; + } + + if (!result[t.categoryId]) { + result[t.categoryId] = { + id: t.categoryId, + minPoint: t.pointReward || 0, + tasks: [t] + }; + } else { + // todo: will update case category not start yet + if (t.status === 0 && (!t.endTime || now < new Date(t.endTime).getTime())) { + result[t.categoryId].minPoint += (t.pointReward || 0); + } + + result[t.categoryId].tasks.push(t); + } + }); + + return result; +} + const Component = ({ className }: Props): React.ReactElement => { useSetCurrentPage('/home/mission'); - const [taskList, setTaskList] = useState(apiSDK.taskList); + const [taskCategoryList, setTaskCategoryList] = useState(apiSDK.taskCategoryList); + const [taskCategoryInfoMap, setTaskCategoryInfoMap] = useState>(getTaskCategoryInfoMap(apiSDK.taskList)); const [account, setAccount] = useState(apiSDK.account); - const { t } = useTranslation(); + const [currentViewMode, setCurrentViewMode] = useState(ViewMode.CATEGORY_LIST); + const [currentTaskCategory, setCurrentTaskCategory] = useState(); useEffect(() => { const accountSub = apiSDK.subscribeAccount().subscribe((data) => { setAccount(data); }); + const taskCategoryListSub = apiSDK.subscribeTaskCategoryList().subscribe((data) => { + setTaskCategoryList(data); + }); + + let taskListUpdaterInterval: NodeJS.Timer; + const taskListSub = apiSDK.subscribeTaskList().subscribe((data) => { - setTaskList(data); + clearInterval(taskListUpdaterInterval); + + setTaskCategoryInfoMap(getTaskCategoryInfoMap(data)); + + taskListUpdaterInterval = setInterval(() => { + setTaskCategoryInfoMap(getTaskCategoryInfoMap(data)); + }, 60000); }); return () => { accountSub.unsubscribe(); + taskCategoryListSub.unsubscribe(); taskListSub.unsubscribe(); + clearInterval(taskListUpdaterInterval); }; }, []); - const sortedTaskList = useMemo(() => { - const now = Date.now(); - - return taskList.sort((a, b) => { - if (a.status < b.status) { - return -1; - } + const onClickCategoryItem = useCallback((categoryId: number) => { + setCurrentViewMode(ViewMode.TASK_LIST); + setCurrentTaskCategory(categoryId); + }, []); - const aDisabled = ((a.startTime && new Date(a.startTime).getTime() > now) || (a.endTime && new Date(a.endTime).getTime() < now)); - const bDisabled = ((b.startTime && new Date(b.startTime).getTime() > now) || (b.endTime && new Date(b.endTime).getTime() < now)); + const onBackToCategoryList = useCallback(() => { + setCurrentViewMode(ViewMode.CATEGORY_LIST); + setCurrentTaskCategory(undefined); + }, []); - if (aDisabled && !bDisabled) { - return 1; + return
+
+ {account && ( + + )} + + { + currentViewMode === ViewMode.CATEGORY_LIST && ( + + ) } - if (!aDisabled && bDisabled) { - return -1; + { + currentViewMode === ViewMode.TASK_LIST && ( + + ) } - - return 0; - }); - }, [taskList]); - - return
-
- {account && } - - {t('Missions')} - - {sortedTaskList.map((task) => ())}
; }; diff --git a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx index a96deb710e6..204a01d9742 100644 --- a/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx +++ b/packages/extension-koni-ui/src/components/Layout/parts/SelectAccount/index.tsx @@ -11,7 +11,7 @@ import { RootState } from '@subwallet/extension-koni-ui/stores'; import { Theme } from '@subwallet/extension-koni-ui/themes'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { copyToClipboard, findAccountByAddress, funcSortByName, isAccountAll, searchAccountFunction } from '@subwallet/extension-koni-ui/utils'; -import {Button, Icon, Image, ModalContext, SelectModal} from '@subwallet/react-ui'; +import { Button, Icon, Image, ModalContext, SelectModal } from '@subwallet/react-ui'; import CN from 'classnames'; import { Copy } from 'phosphor-react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -35,7 +35,7 @@ const rankIconMap: Record = { silver: '/images/ranks/silver.svg', gold: '/images/ranks/gold.svg', platinum: '/images/ranks/platinum.svg', - diamond: '/images/ranks/diamond.svg', + diamond: '/images/ranks/diamond.svg' }; function Component ({ className }: Props): React.ReactElement { @@ -48,7 +48,7 @@ function Component ({ className }: Props): React.ReactElement { const [gameAccount, setGameAccount] = useState(apiSDK.account); - const { accounts: _accounts, currentAccount, isAllAccount } = useSelector((state: RootState) => state.accountState); + const { accounts: _accounts, currentAccount } = useSelector((state: RootState) => state.accountState); const [selectedQrAddress, setSelectedQrAddress] = useState(); diff --git a/packages/extension-koni-ui/src/connector/booka/sdk.ts b/packages/extension-koni-ui/src/connector/booka/sdk.ts index d24769d8405..e711b3ec246 100644 --- a/packages/extension-koni-ui/src/connector/booka/sdk.ts +++ b/packages/extension-koni-ui/src/connector/booka/sdk.ts @@ -3,7 +3,7 @@ import { SWStorage } from '@subwallet/extension-base/storage'; import { createPromiseHandler } from '@subwallet/extension-base/utils'; -import { BookaAccount, Game, GamePlay, LeaderboardPerson, ReferralRecord, Task } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { BookaAccount, Game, GamePlay, LeaderboardPerson, ReferralRecord, Task, TaskCategory } from '@subwallet/extension-koni-ui/connector/booka/types'; import { TelegramConnector } from '@subwallet/extension-koni-ui/connector/telegram'; import { signRaw } from '@subwallet/extension-koni-ui/messaging'; import fetch from 'cross-fetch'; @@ -16,6 +16,7 @@ const telegramConnector = TelegramConnector.instance; const CACHE_KEYS = { account: 'data--account-cache', + taskCategoryList: 'data--task-category-list-cache', taskList: 'data--task-list-cache', gameList: 'data--game-list-cache' }; @@ -24,13 +25,14 @@ export class BookaSdk { private syncHandler = createPromiseHandler(); private accountSubject = new BehaviorSubject(undefined); private taskListSubject = new BehaviorSubject([]); + private taskCategoryListSubject = new BehaviorSubject([]); private gameListSubject = new BehaviorSubject([]); private currentGamePlaySubject = new BehaviorSubject(undefined); private leaderBoardSubject = new BehaviorSubject([]); private referralListSubject = new BehaviorSubject([]); constructor () { - storage.getItems(Object.values(CACHE_KEYS)).then(([account, tasks, game]) => { + storage.getItems(Object.values(CACHE_KEYS)).then(([account, taskCategory, tasks, game]) => { if (account) { try { const accountData = JSON.parse(account) as BookaAccount; @@ -41,6 +43,16 @@ export class BookaSdk { } } + if (taskCategory) { + try { + const taskCategoryList = JSON.parse(taskCategory) as TaskCategory[]; + + this.taskCategoryListSubject.next(taskCategoryList); + } catch (e) { + console.error('Failed to parse task list', e); + } + } + if (tasks) { try { const taskList = JSON.parse(tasks) as Task[]; @@ -75,6 +87,10 @@ export class BookaSdk { return this.taskListSubject.value; } + public get taskCategoryList () { + return this.taskCategoryListSubject.value; + } + public get gameList () { return this.gameListSubject.value; } @@ -159,6 +175,20 @@ export class BookaSdk { return this.gameListSubject; } + async fetchTaskCategoryList () { + await this.waitForSync; + const taskCategoryList = await this.getRequest(`${GAME_API_HOST}/api/task-category/fetch`); + + if (taskCategoryList) { + this.taskCategoryListSubject.next(taskCategoryList); + storage.setItem(CACHE_KEYS.taskCategoryList, JSON.stringify(taskCategoryList)).catch(console.error); + } + } + + subscribeTaskCategoryList () { + return this.taskCategoryListSubject; + } + async fetchTaskList () { await this.waitForSync; const taskList = await this.getRequest(`${GAME_API_HOST}/api/task/history`); @@ -176,6 +206,8 @@ export class BookaSdk { async finishTask (taskId: number) { await this.postRequest(`${GAME_API_HOST}/api/task/submit`, { taskId }); + await this.fetchTaskCategoryList(); + await this.fetchTaskList(); await this.reloadAccount(); @@ -230,7 +262,7 @@ export class BookaSdk { storage.setItem(CACHE_KEYS.account, JSON.stringify(account)).catch(console.error); this.syncHandler.resolve(); - await Promise.all([this.fetchGameList(), this.fetchTaskList(), this.fetchLeaderboard()]); + await Promise.all([this.fetchGameList(), this.fetchTaskCategoryList(), this.fetchTaskList(), this.fetchLeaderboard()]); } else { throw new Error('Failed to sync account'); } diff --git a/packages/extension-koni-ui/src/connector/booka/types.ts b/packages/extension-koni-ui/src/connector/booka/types.ts index 58d61df8ac2..b0b70b7795d 100644 --- a/packages/extension-koni-ui/src/connector/booka/types.ts +++ b/packages/extension-koni-ui/src/connector/booka/types.ts @@ -27,23 +27,41 @@ export interface Game { export interface Task { id: number; // id on db - gameId: number; contentId: number; - url: string; slug: string; - name: string; - description: string; - icon: string; - pointReward: number; - itemReward: number; - startTime?: string; - endTime?: string; - interval?: number; + gameId?: number | null; + categoryId?: number | null; + url?: string | null; + name?: string | null; + description?: string | null; + icon?: string | null; + pointReward?: number | null; + itemReward?: number | null; + startTime?: string | null; + endTime?: string | null; + interval?: number | null; status: number; completedAt?: string; } +export interface TaskCategory { + id: number; // id on db + contentId: number; + slug: string; + name?: string | null; + description?: string | null; + icon?: string | null; + active: boolean; + minPoint?: number; +} + +export type TaskCategoryInfo = { + id: number; + minPoint: number; + tasks: Task[]; +} + export interface GamePlay { id: number; // id on db gameId: number; diff --git a/packages/extension-koni-ui/src/contexts/DataContext.tsx b/packages/extension-koni-ui/src/contexts/DataContext.tsx index bbb3023ca4f..df0342c1dd2 100644 --- a/packages/extension-koni-ui/src/contexts/DataContext.tsx +++ b/packages/extension-koni-ui/src/contexts/DataContext.tsx @@ -1,12 +1,10 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { useSelector } from '@subwallet/extension-koni-ui/hooks'; import { persistor, store, StoreName } from '@subwallet/extension-koni-ui/stores'; import { getMissionPoolData, subscribeAccountsData, subscribeAddressBook, subscribeAssetLogoMaps, subscribeAssetRegistry, subscribeAssetSettings, subscribeAuthorizeRequests, subscribeAuthUrls, subscribeBalance, subscribeBuyServices, subscribeBuyTokens, subscribeChainInfoMap, subscribeChainLogoMaps, subscribeChainStakingMetadata, subscribeChainStateMap, subscribeChainStatusMap, subscribeConfirmationRequests, subscribeConnectWCRequests, subscribeKeyringState, subscribeMantaPayConfig, subscribeMantaPaySyncingState, subscribeMetadataRequests, subscribeMultiChainAssetMap, subscribeNftCollections, subscribeNftItems, subscribePrice, subscribeProcessingCampaign, subscribeSigningRequests, subscribeStaking, subscribeStakingNominatorMetadata, subscribeStakingReward, subscribeTransactionRequests, subscribeTxHistory, subscribeUiSettings, subscribeWalletConnectSessions, subscribeWCNotSupportRequests, subscribeXcmRefMap } from '@subwallet/extension-koni-ui/stores/utils'; -import { isAccountAll } from '@subwallet/extension-koni-ui/utils'; import Bowser from 'bowser'; -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; From c53319cd4c8aed22bca32ba172f8322faa4c2d58 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 9 May 2024 10:15:25 +0700 Subject: [PATCH 02/13] [Mission] Resort task list --- .../src/Popup/Home/Mission/TaskList.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskList.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskList.tsx index 8f4826661ad..1289f2a6660 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskList.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskList.tsx @@ -25,10 +25,6 @@ const Component = ({ className, currentTaskCategory, onBackToCategoryList, taskC const taskList = currentTaskCategory ? taskCategoryInfoMap[currentTaskCategory]?.tasks || [] : []; return taskList.sort((a, b) => { - if (a.status < b.status) { - return -1; - } - const aDisabled = ((a.startTime && new Date(a.startTime).getTime() > now) || (a.endTime && new Date(a.endTime).getTime() < now)); const bDisabled = ((b.startTime && new Date(b.startTime).getTime() > now) || (b.endTime && new Date(b.endTime).getTime() < now)); @@ -40,7 +36,15 @@ const Component = ({ className, currentTaskCategory, onBackToCategoryList, taskC return -1; } - return 0; + if (a.status === 0 && b.status !== 0) { + return -1; + } + + if (a.status !== 0 && b.status === 0) { + return 1; + } + + return a.status - b.status; }); }, [currentTaskCategory, taskCategoryInfoMap]); From 1ea24ac309a4608dbae83775e7fe59967cc75996 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 9 May 2024 10:26:10 +0700 Subject: [PATCH 03/13] [Mission] Update min point calculation for task category --- .../src/Popup/Home/Mission/index.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx index 30d9b1857d1..88d8ebe18c5 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/index.tsx @@ -37,12 +37,21 @@ function getTaskCategoryInfoMap (tasks: Task[]): Record 0) { + return; } - result[t.categoryId].tasks.push(t); + if (t.startTime && (now < new Date(t.startTime).getTime())) { + return; + } + + if (t.endTime && (now >= new Date(t.endTime).getTime())) { + return; + } + + result[t.categoryId].minPoint += (t.pointReward || 0); } }); @@ -75,7 +84,7 @@ const Component = ({ className }: Props): React.ReactElement => { taskListUpdaterInterval = setInterval(() => { setTaskCategoryInfoMap(getTaskCategoryInfoMap(data)); - }, 60000); + }, 10000); }); return () => { From 3b16209c140ee938399833ae49e330d783f5bdb5 Mon Sep 17 00:00:00 2001 From: lw Date: Thu, 9 May 2024 11:42:18 +0700 Subject: [PATCH 04/13] [Mission] Add caret for task category item --- .../Popup/Home/Mission/TaskCategoryList.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx index 08649539771..faee880606a 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx @@ -5,7 +5,8 @@ import { TaskCategory, TaskCategoryInfo } from '@subwallet/extension-koni-ui/con import { useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; import { formatInteger } from '@subwallet/extension-koni-ui/utils'; -import { Image, Typography } from '@subwallet/react-ui'; +import { Icon, Image, Typography } from '@subwallet/react-ui'; +import { CaretRight } from 'phosphor-react'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -48,13 +49,19 @@ const Component = ({ className, onClickCategoryItem, taskCategoryInfoMap, taskCa src={tc.icon || undefined} width={40} > -
+
{tc.name}
Min point can earn: {formatInteger(taskCategoryInfoMap[tc.id]?.minPoint || 0)}
+
+ +
)) } @@ -78,6 +85,17 @@ export const TaskCategoryList = styled(Component)(({ theme: { extend alignItems: 'center' }, + '.task-category-item-content': { + flex: 1 + }, + + '.task-category-item-caret-icon': { + minWidth: 40, + marginRight: -token.marginXS, + display: 'flex', + justifyContent: 'center' + }, + '.task-category-item + .task-category-item': { marginTop: token.marginXS } From 48aeb78aadc6340ff456ddb6a1ff18f00a6f5b1e Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 13 May 2024 09:42:00 +0700 Subject: [PATCH 05/13] [Shop] Update shop flow --- .../src/Popup/Home/Games/index.tsx | 79 +++++++++++-- .../src/components/Modal/Shop/ShopModal.tsx | 110 ++++++++++++++++++ .../src/components/Modal/Shop/index.tsx | 4 + .../src/components/Modal/index.tsx | 1 + .../src/components/Shop/ShopItem.tsx | 89 ++++++++++++++ .../src/components/Shop/index.ts | 4 + .../extension-koni-ui/src/components/index.ts | 2 +- .../src/connector/booka/sdk.ts | 87 +++++++++++++- .../src/connector/booka/types.ts | 40 +++++++ packages/extension-koni-ui/src/types/shop.ts | 16 +++ 10 files changed, 418 insertions(+), 14 deletions(-) create mode 100644 packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx create mode 100644 packages/extension-koni-ui/src/components/Modal/Shop/index.tsx create mode 100644 packages/extension-koni-ui/src/components/Shop/ShopItem.tsx create mode 100644 packages/extension-koni-ui/src/components/Shop/index.ts create mode 100644 packages/extension-koni-ui/src/types/shop.ts diff --git a/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx index d863d5ce72f..3808196211c 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx @@ -1,16 +1,19 @@ // Copyright 2019-2022 @subwallet/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { ShopModal } from '@subwallet/extension-koni-ui/components'; import GameAccount from '@subwallet/extension-koni-ui/components/Games/GameAccount'; import GameEnergy from '@subwallet/extension-koni-ui/components/Games/GameEnergy'; +import { ShopModalId } from '@subwallet/extension-koni-ui/components/Modal/Shop/ShopModal'; import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; -import { Game } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { Game, GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types'; import { useSetCurrentPage, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { GameApp } from '@subwallet/extension-koni-ui/Popup/Home/Games/gameSDK'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; -import { Button, Image, Typography } from '@subwallet/react-ui'; +import { Button, Icon, Image, ModalContext, Typography } from '@subwallet/react-ui'; import CN from 'classnames'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { ShoppingBag } from 'phosphor-react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; type Props = ThemeProps; @@ -28,10 +31,16 @@ function checkComingSoon (game: Game): boolean { return gameStartTime > Date.now(); } +const shopModalId = ShopModalId; + const Component = ({ className }: Props): React.ReactElement => { useSetCurrentPage('/home/games'); const gameIframe = useRef(null); const [gameList, setGameList] = useState(apiSDK.gameList); + const [gameItemMap, setGameItemMap] = useState>(apiSDK.gameItemMap); + const [gameInventoryItemList, setGameInventoryItemList] = useState(apiSDK.gameInventoryItemList); + const [currentGameShopId, setCurrentGameShopId] = useState(); + const { activeModal } = useContext(ModalContext); const [account, setAccount] = useState(apiSDK.account); const [currentGame, setCurrentGame] = useState(undefined); const { t } = useTranslation(); @@ -67,6 +76,13 @@ const Component = ({ className }: Props): React.ReactElement => { }; }, [exitGame]); + const onOpenShop = useCallback((gameId?: number) => { + return () => { + setCurrentGameShopId(gameId); + activeModal(shopModalId); + }; + }, [activeModal]); + useEffect(() => { const accountSub = apiSDK.subscribeAccount().subscribe((data) => { setAccount(data); @@ -76,9 +92,19 @@ const Component = ({ className }: Props): React.ReactElement => { setGameList(data); }); + const gameItemMapSub = apiSDK.subscribeGameItemMap().subscribe((data) => { + setGameItemMap(data); + }); + + const gameInventoryItemListSub = apiSDK.subscribeGameInventoryItemList().subscribe((data) => { + setGameInventoryItemList(data); + }); + return () => { accountSub.unsubscribe(); gameListSub.unsubscribe(); + gameItemMapSub.unsubscribe(); + gameInventoryItemListSub.unsubscribe(); }; }, []); @@ -94,6 +120,18 @@ const Component = ({ className }: Props): React.ReactElement => { energy={account.attributes.energy} startTime={account.attributes.lastEnergyUpdated} /> + +
} {gameList.map((game) => (
{
- +
+ +
+ { src={currentGame.url} />
} + +
; }; diff --git a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx new file mode 100644 index 00000000000..eb749b656b8 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx @@ -0,0 +1,110 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ShopItem } from '@subwallet/extension-koni-ui/components'; +import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; +import { GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types'; +import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { ShopItemInfo } from '@subwallet/extension-koni-ui/types/shop'; +import { ModalContext, SwModal } from '@subwallet/react-ui'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & { + gameId?: number; + gameItemMap: Record; + gameInventoryItemList: GameInventoryItem[]; +}; + +export const ShopModalId = 'ShopModalId'; +const apiSDK = BookaSdk.instance; + +function Component ({ className, gameId, + gameInventoryItemList, + gameItemMap }: Props): React.ReactElement { + const { t } = useTranslation(); + const [buyLoading, setBuyLoading] = useState(false); + + const { inactiveModal } = useContext(ModalContext); + + const onClose = useCallback(() => { + inactiveModal(ShopModalId); + }, [inactiveModal]); + + console.log('gameItemMap', gameItemMap, gameInventoryItemList); + + const items = useMemo(() => { + const result: ShopItemInfo[] = []; + + const inventoryItemMapByGameItemId: Record = {}; + + gameInventoryItemList.forEach((i) => { + inventoryItemMapByGameItemId[i.gameItemId] = i; + }); + + [...Object.values(gameItemMap)].forEach((group) => { + group.forEach((gi) => { + if ((!gameId && !gi.gameId) || (gameId && gi.gameId === gameId)) { + const limit = gi.maxBuy || undefined; + const inventoryQuantity = inventoryItemMapByGameItemId[gi.id]?.quantity || undefined; + + result.push({ + gameItemId: gi.id, + name: gi.name, + gameId: gi.gameId, + limit, + description: gi.description, + inventoryQuantity, + itemGroup: gi.itemGroup, + itemGroupLevel: gi.itemGroupLevel, + price: gi.price, + disabled: (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1) + }); + } + }); + }); + + return result; + }, [gameId, gameInventoryItemList, gameItemMap]); + + const onBuy = useCallback((gameItemId: number) => { + setBuyLoading(true); + apiSDK.buyItem(gameItemId).catch((e) => { + console.log('buyItem error', e); + }).finally(() => { + setBuyLoading(false); + }); + }, []); + + return ( + + { + items.map((item) => ( + + )) + } + + ); +} + +const ShopModal = styled(Component)(({ theme: { token } }: Props) => { + return ({ + '.shop-item + .shop-item': { + marginTop: token.marginSM + } + }); +}); + +export default ShopModal; diff --git a/packages/extension-koni-ui/src/components/Modal/Shop/index.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/index.tsx new file mode 100644 index 00000000000..aeaba09f14a --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Shop/index.tsx @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { default as ShopModal } from './ShopModal'; diff --git a/packages/extension-koni-ui/src/components/Modal/index.tsx b/packages/extension-koni-ui/src/components/Modal/index.tsx index da360635ee4..eb153e9170d 100644 --- a/packages/extension-koni-ui/src/components/Modal/index.tsx +++ b/packages/extension-koni-ui/src/components/Modal/index.tsx @@ -17,3 +17,4 @@ export * from './GlobalSearchTokenModal'; export * from './ReceiveModal'; export * from './Common'; export * from './Announcement'; +export * from './Shop'; diff --git a/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx new file mode 100644 index 00000000000..699cf016349 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx @@ -0,0 +1,89 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import DefaultLogosMap from '@subwallet/extension-koni-ui/assets/logo'; +import { ThemeProps } from '@subwallet/extension-koni-ui/types'; +import { ShopItemInfo } from '@subwallet/extension-koni-ui/types/shop'; +import { Button, Image } from '@subwallet/react-ui'; +import CN from 'classnames'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +type Props = ThemeProps & ShopItemInfo & { + onBuy: (gameItemId: number, quantity?: number) => void +}; + +function Component (props: Props): React.ReactElement { + const { className = '', + description, + disabled, + gameItemId, + inventoryQuantity, + limit, + name, onBuy, price } = props; + + const _onBuy = useCallback(() => { + onBuy(gameItemId, 1); + }, [gameItemId, onBuy]); + + return ( +
+ + +
+
{name}
+
{description}
+ { + !!limit && ( +
Limit: {limit}
+ ) + } + +
Price: {price}
+ + { + !!inventoryQuantity && ( +
Quantity: {inventoryQuantity}
+ ) + } + +
+ + +
+ ); +} + +const ShopItem = styled(Component)(({ theme: { token } }: Props) => { + return ({ + display: 'flex', + backgroundColor: token.colorBgSecondary, + padding: token.paddingSM, + borderRadius: token.borderRadiusLG, + + '.item-icon': { + marginRight: token.marginSM + }, + + '.__middle-part': { + flex: 1 + } + }); +}); + +export default ShopItem; diff --git a/packages/extension-koni-ui/src/components/Shop/index.ts b/packages/extension-koni-ui/src/components/Shop/index.ts new file mode 100644 index 00000000000..2632b841b40 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Shop/index.ts @@ -0,0 +1,4 @@ +// Copyright 2019-2022 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { default as ShopItem } from './ShopItem'; diff --git a/packages/extension-koni-ui/src/components/index.ts b/packages/extension-koni-ui/src/components/index.ts index 5b5a4a43039..57de0080d05 100644 --- a/packages/extension-koni-ui/src/components/index.ts +++ b/packages/extension-koni-ui/src/components/index.ts @@ -31,4 +31,4 @@ export * from './Setting'; export * from './StakingItem'; export * from './TokenItem'; export * from './WalletConnect'; -export * from './Crowdloan'; +export * from './Shop'; diff --git a/packages/extension-koni-ui/src/connector/booka/sdk.ts b/packages/extension-koni-ui/src/connector/booka/sdk.ts index e711b3ec246..870011ac478 100644 --- a/packages/extension-koni-ui/src/connector/booka/sdk.ts +++ b/packages/extension-koni-ui/src/connector/booka/sdk.ts @@ -3,7 +3,7 @@ import { SWStorage } from '@subwallet/extension-base/storage'; import { createPromiseHandler } from '@subwallet/extension-base/utils'; -import { BookaAccount, Game, GamePlay, LeaderboardPerson, ReferralRecord, Task, TaskCategory } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { BookaAccount, EnergyConfig, Game, GameInventoryItem, GameItem, GamePlay, LeaderboardPerson, ReferralRecord, Task, TaskCategory } from '@subwallet/extension-koni-ui/connector/booka/types'; import { TelegramConnector } from '@subwallet/extension-koni-ui/connector/telegram'; import { signRaw } from '@subwallet/extension-koni-ui/messaging'; import fetch from 'cross-fetch'; @@ -18,7 +18,8 @@ const CACHE_KEYS = { account: 'data--account-cache', taskCategoryList: 'data--task-category-list-cache', taskList: 'data--task-list-cache', - gameList: 'data--game-list-cache' + gameList: 'data--game-list-cache', + energyConfig: 'data--energy-config' }; export class BookaSdk { @@ -30,9 +31,12 @@ export class BookaSdk { private currentGamePlaySubject = new BehaviorSubject(undefined); private leaderBoardSubject = new BehaviorSubject([]); private referralListSubject = new BehaviorSubject([]); + private gameItemMapSubject = new BehaviorSubject>({}); + private gameInventoryItemListSubject = new BehaviorSubject([]); + private energyConfigSubject = new BehaviorSubject(undefined); constructor () { - storage.getItems(Object.values(CACHE_KEYS)).then(([account, taskCategory, tasks, game]) => { + storage.getItems(Object.values(CACHE_KEYS)).then(([account, taskCategory, tasks, game, energyConfig]) => { if (account) { try { const accountData = JSON.parse(account) as BookaAccount; @@ -72,6 +76,16 @@ export class BookaSdk { console.error('Failed to parse game list', e); } } + + if (energyConfig) { + try { + const _energyConfig = JSON.parse(energyConfig) as EnergyConfig; + + this.energyConfigSubject.next(_energyConfig); + } catch (e) { + console.error('Failed to parse energy config', e); + } + } }).catch(console.error); } @@ -83,6 +97,10 @@ export class BookaSdk { return this.accountSubject.value; } + public get energyConfig () { + return this.energyConfigSubject.value; + } + public get taskList () { return this.taskListSubject.value; } @@ -95,6 +113,14 @@ export class BookaSdk { return this.gameListSubject.value; } + public get gameItemMap () { + return this.gameItemMapSubject.value; + } + + public get gameInventoryItemList () { + return this.gameInventoryItemListSubject.value; + } + public get leaderBoard () { return this.leaderBoardSubject.value; } @@ -162,6 +188,19 @@ export class BookaSdk { return this.accountSubject; } + async fetchEnergyConfig () { + const energyConfig = await this.getRequest(`${GAME_API_HOST}/api/shop/get-config-buy-energy`); + + if (energyConfig) { + this.energyConfigSubject.next(energyConfig); + storage.setItem(CACHE_KEYS.energyConfig, JSON.stringify(energyConfig)).catch(console.error); + } + } + + subscribeEnergyConfig () { + return this.energyConfigSubject; + } + async fetchGameList () { const gameList = await this.getRequest(`${GAME_API_HOST}/api/game/fetch`); @@ -330,6 +369,48 @@ export class BookaSdk { await Promise.all([this.reloadAccount()]); } + // --- shop + + async fetchGameItemMap () { + await this.waitForSync; + + const gameItemMap = await this.postRequest>(`${GAME_API_HOST}/api/shop/list-items`, {}); + + if (gameItemMap) { + this.gameItemMapSubject.next(gameItemMap); + } + } + + subscribeGameItemMap () { + return this.gameItemMapSubject; + } + + async fetchGameInventoryItemList () { + await this.waitForSync; + + const inventoryItemList = await this.getRequest(`${GAME_API_HOST}/api/shop/get-inventory`); + + if (inventoryItemList) { + this.gameInventoryItemListSubject.next(inventoryItemList); + } + } + + subscribeGameInventoryItemList () { + return this.gameInventoryItemListSubject; + } + + async buyItem (gameItemId: number, quantity = 1) { + await this.postRequest(`${GAME_API_HOST}/api/shop/buy-item`, { gameItemId, quantity }); + + await this.fetchGameInventoryItemList(); + + await this.fetchGameItemMap(); + + await this.reloadAccount(); + } + + // --- shop + async fetchLeaderboard () { await this.waitForSync; const leaderBoard = await this.getRequest(`${GAME_API_HOST}/api/game/leader-board`); diff --git a/packages/extension-koni-ui/src/connector/booka/types.ts b/packages/extension-koni-ui/src/connector/booka/types.ts index b0b70b7795d..a0bad5ac7c8 100644 --- a/packages/extension-koni-ui/src/connector/booka/types.ts +++ b/packages/extension-koni-ui/src/connector/booka/types.ts @@ -7,6 +7,46 @@ export enum EventTypeEnum { EVENT = 'EVENT', } +export interface EnergyConfig { + energyPrice: number, + energyBuyLimit: number, + maxEnergy: number, + energyOneBuy: number +} + +export interface GameItem { + id: number, + contentId: number, + gameId: number, + slug: string, + name: string, + description: string, + price: number, + tokenPrice: number, + maxBuy?: number | null, + maxBuyDaily: number, + itemGroup: string, + itemGroupLevel: number, + effectDuration: number, +} + +export enum GameInventoryItemStatus { + INACTIVE = 'inactive', // After buy item request + ACTIVE = 'active', // After validate signature + USED = 'used', // After used item +} + +export interface GameInventoryItem { + id: number, + gameId: number, + accountId: number, + gameDataId: number, + gameItemId: number, + quantity: number, + usable: boolean, + itemId?: number | null +} + export interface Game { id: number; contentId: number; diff --git a/packages/extension-koni-ui/src/types/shop.ts b/packages/extension-koni-ui/src/types/shop.ts new file mode 100644 index 00000000000..6ac79893c38 --- /dev/null +++ b/packages/extension-koni-ui/src/types/shop.ts @@ -0,0 +1,16 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export type ShopItemInfo = { + icon?: string; + gameItemId: number; + name: string; + gameId?: number; + limit?: number; + description?: string; + inventoryQuantity?: number; + itemGroup?: string; + itemGroupLevel?: number; + price: number; + disabled?: boolean; +} From 4ad31aeb0d111cfbcfb1bdce7da38995b498352b Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 13 May 2024 17:49:00 +0700 Subject: [PATCH 06/13] [Shop] Update group item --- .../src/components/Modal/Shop/ShopModal.tsx | 67 +++++++++++++------ .../src/connector/booka/sdk.ts | 2 +- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx index eb749b656b8..d0dad0bbb06 100644 --- a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx @@ -32,8 +32,6 @@ function Component ({ className, gameId, inactiveModal(ShopModalId); }, [inactiveModal]); - console.log('gameItemMap', gameItemMap, gameInventoryItemList); - const items = useMemo(() => { const result: ShopItemInfo[] = []; @@ -43,24 +41,55 @@ function Component ({ className, gameId, inventoryItemMapByGameItemId[i.gameItemId] = i; }); - [...Object.values(gameItemMap)].forEach((group) => { - group.forEach((gi) => { + const getShopItem = (gi: GameItem, disabled = false): ShopItemInfo => { + const limit = gi.maxBuy || undefined; + const inventoryQuantity = inventoryItemMapByGameItemId[gi.id]?.quantity || undefined; + + return { + gameItemId: gi.id, + name: gi.name, + gameId: gi.gameId, + limit, + description: gi.description, + inventoryQuantity, + itemGroup: gi.itemGroup, + itemGroupLevel: gi.itemGroupLevel, + price: gi.price, + disabled: disabled || (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1) + }; + }; + + [...Object.keys(gameItemMap)].forEach((groupKey) => { + if (groupKey !== 'NO_GROUP' && gameItemMap[groupKey][0]?.effectDuration === -1) { + const noQuantityItems = gameItemMap[groupKey].filter((gi) => !inventoryItemMapByGameItemId[gi.id]?.quantity); + + let itemPresentForGroup: GameItem; + + if (noQuantityItems.length) { + itemPresentForGroup = noQuantityItems.reduce((item, currentItem) => { + return currentItem.itemGroupLevel && item.itemGroupLevel && currentItem.itemGroupLevel < item.itemGroupLevel ? currentItem : item; + }, { itemGroupLevel: Number.POSITIVE_INFINITY } as GameItem); + + if (itemPresentForGroup.itemGroupLevel !== Number.POSITIVE_INFINITY) { + result.push(getShopItem(itemPresentForGroup)); + } + } else { + itemPresentForGroup = gameItemMap[groupKey] + .reduce((item, currentItem) => { + return currentItem.itemGroupLevel && item.itemGroupLevel && currentItem.itemGroupLevel > item.itemGroupLevel ? currentItem : item; + }, { itemGroupLevel: Number.NEGATIVE_INFINITY } as GameItem); + + if (itemPresentForGroup.itemGroupLevel !== Number.NEGATIVE_INFINITY) { + result.push(getShopItem(itemPresentForGroup, true)); + } + } + + return; + } + + gameItemMap[groupKey].forEach((gi) => { if ((!gameId && !gi.gameId) || (gameId && gi.gameId === gameId)) { - const limit = gi.maxBuy || undefined; - const inventoryQuantity = inventoryItemMapByGameItemId[gi.id]?.quantity || undefined; - - result.push({ - gameItemId: gi.id, - name: gi.name, - gameId: gi.gameId, - limit, - description: gi.description, - inventoryQuantity, - itemGroup: gi.itemGroup, - itemGroupLevel: gi.itemGroupLevel, - price: gi.price, - disabled: (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1) - }); + result.push(getShopItem(gi)); } }); }); diff --git a/packages/extension-koni-ui/src/connector/booka/sdk.ts b/packages/extension-koni-ui/src/connector/booka/sdk.ts index 870011ac478..a71c6b8d84d 100644 --- a/packages/extension-koni-ui/src/connector/booka/sdk.ts +++ b/packages/extension-koni-ui/src/connector/booka/sdk.ts @@ -301,7 +301,7 @@ export class BookaSdk { storage.setItem(CACHE_KEYS.account, JSON.stringify(account)).catch(console.error); this.syncHandler.resolve(); - await Promise.all([this.fetchGameList(), this.fetchTaskCategoryList(), this.fetchTaskList(), this.fetchLeaderboard()]); + await Promise.all([this.fetchGameList(), this.fetchTaskCategoryList(), this.fetchTaskList(), this.fetchLeaderboard(), this.fetchGameItemMap(), this.fetchGameInventoryItemList()]); } else { throw new Error('Failed to sync account'); } From dca56f6a0e19c56877223a4d24985bf7f7104efa Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 13 May 2024 18:10:59 +0700 Subject: [PATCH 07/13] [Shop] Update energy item --- .../src/Popup/Home/Games/index.tsx | 10 ++++++++- .../src/components/Games/GameEnergy.tsx | 14 +++++++++--- .../src/components/Modal/Shop/ShopModal.tsx | 22 ++++++++++++++----- .../src/connector/booka/sdk.ts | 10 ++++++++- packages/extension-koni-ui/src/types/shop.ts | 3 ++- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx b/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx index 3808196211c..9f2985b2303 100644 --- a/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx +++ b/packages/extension-koni-ui/src/Popup/Home/Games/index.tsx @@ -6,7 +6,7 @@ import GameAccount from '@subwallet/extension-koni-ui/components/Games/GameAccou import GameEnergy from '@subwallet/extension-koni-ui/components/Games/GameEnergy'; import { ShopModalId } from '@subwallet/extension-koni-ui/components/Modal/Shop/ShopModal'; import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk'; -import { Game, GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types'; +import { EnergyConfig, Game, GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types'; import { useSetCurrentPage, useTranslation } from '@subwallet/extension-koni-ui/hooks'; import { GameApp } from '@subwallet/extension-koni-ui/Popup/Home/Games/gameSDK'; import { ThemeProps } from '@subwallet/extension-koni-ui/types'; @@ -37,6 +37,7 @@ const Component = ({ className }: Props): React.ReactElement => { useSetCurrentPage('/home/games'); const gameIframe = useRef(null); const [gameList, setGameList] = useState(apiSDK.gameList); + const [energyConfig, setEnergyConfig] = useState(apiSDK.energyConfig); const [gameItemMap, setGameItemMap] = useState>(apiSDK.gameItemMap); const [gameInventoryItemList, setGameInventoryItemList] = useState(apiSDK.gameInventoryItemList); const [currentGameShopId, setCurrentGameShopId] = useState(); @@ -92,6 +93,10 @@ const Component = ({ className }: Props): React.ReactElement => { setGameList(data); }); + const energyConfigSub = apiSDK.subscribeEnergyConfig().subscribe((data) => { + setEnergyConfig(data); + }); + const gameItemMapSub = apiSDK.subscribeGameItemMap().subscribe((data) => { setGameItemMap(data); }); @@ -102,6 +107,7 @@ const Component = ({ className }: Props): React.ReactElement => { return () => { accountSub.unsubscribe(); + energyConfigSub.unsubscribe(); gameListSub.unsubscribe(); gameItemMapSub.unsubscribe(); gameInventoryItemListSub.unsubscribe(); @@ -118,6 +124,7 @@ const Component = ({ className }: Props): React.ReactElement => { /> @@ -211,6 +218,7 @@ const Component = ({ className }: Props): React.ReactElement => { } (); const [currentEnergy, setCurrentEnergy] = useState(energy); const intervalRef = useRef(null); @@ -31,6 +31,10 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) { }, [startTime]); const updateEnergy = useCallback(() => { + if (!maxEnergy) { + return; + } + const now = Date.now(); const diff = now - startRegen; const recovered = Math.floor(diff / regenTime); @@ -48,7 +52,7 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) { setCurrentEnergy(recovered + energy); setCountdown(remainingTime); } - }, [energy, startRegen]); + }, [energy, maxEnergy, startRegen]); useEffect(() => { intervalRef.current = setInterval(updateEnergy, ONE_SECOND); @@ -60,6 +64,10 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) { }; }, [updateEnergy]); + if (!maxEnergy) { + return null; + } + return
; gameInventoryItemList: GameInventoryItem[]; }; @@ -20,9 +21,9 @@ type Props = ThemeProps & { export const ShopModalId = 'ShopModalId'; const apiSDK = BookaSdk.instance; -function Component ({ className, gameId, - gameInventoryItemList, - gameItemMap }: Props): React.ReactElement { +function Component ({ className, energyConfig, + gameId, + gameInventoryItemList, gameItemMap }: Props): React.ReactElement { const { t } = useTranslation(); const [buyLoading, setBuyLoading] = useState(false); @@ -35,6 +36,17 @@ function Component ({ className, gameId, const items = useMemo(() => { const result: ShopItemInfo[] = []; + if (energyConfig) { + result.push({ + gameItemId: 'buy-energy-id', + name: 'Energy', + limit: energyConfig.energyBuyLimit, + description: '', + price: energyConfig.energyPrice, + isEnergy: true + }); + } + const inventoryItemMapByGameItemId: Record = {}; gameInventoryItemList.forEach((i) => { @@ -46,7 +58,7 @@ function Component ({ className, gameId, const inventoryQuantity = inventoryItemMapByGameItemId[gi.id]?.quantity || undefined; return { - gameItemId: gi.id, + gameItemId: `${gi.id}`, name: gi.name, gameId: gi.gameId, limit, diff --git a/packages/extension-koni-ui/src/connector/booka/sdk.ts b/packages/extension-koni-ui/src/connector/booka/sdk.ts index a71c6b8d84d..a10abbc8f18 100644 --- a/packages/extension-koni-ui/src/connector/booka/sdk.ts +++ b/packages/extension-koni-ui/src/connector/booka/sdk.ts @@ -301,7 +301,15 @@ export class BookaSdk { storage.setItem(CACHE_KEYS.account, JSON.stringify(account)).catch(console.error); this.syncHandler.resolve(); - await Promise.all([this.fetchGameList(), this.fetchTaskCategoryList(), this.fetchTaskList(), this.fetchLeaderboard(), this.fetchGameItemMap(), this.fetchGameInventoryItemList()]); + await Promise.all([ + this.fetchEnergyConfig(), + this.fetchGameList(), + this.fetchTaskCategoryList(), + this.fetchTaskList(), + this.fetchLeaderboard(), + this.fetchGameItemMap(), + this.fetchGameInventoryItemList() + ]); } else { throw new Error('Failed to sync account'); } diff --git a/packages/extension-koni-ui/src/types/shop.ts b/packages/extension-koni-ui/src/types/shop.ts index 6ac79893c38..d2d9035d278 100644 --- a/packages/extension-koni-ui/src/types/shop.ts +++ b/packages/extension-koni-ui/src/types/shop.ts @@ -3,7 +3,8 @@ export type ShopItemInfo = { icon?: string; - gameItemId: number; + gameItemId: string; + isEnergy?: boolean; name: string; gameId?: number; limit?: number; From d423fa0e78c76293ccfcac0168739d42fa1e75b0 Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 13 May 2024 18:39:40 +0700 Subject: [PATCH 08/13] [Shop] Update buy energy --- .../src/components/Modal/Shop/ShopModal.tsx | 27 ++++++++++++------- .../src/components/Shop/ShopItem.tsx | 2 +- .../src/connector/booka/sdk.ts | 15 +++++++++++ packages/extension-koni-ui/src/types/shop.ts | 2 +- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx index 7a225345395..7685f479c41 100644 --- a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx @@ -40,10 +40,8 @@ function Component ({ className, energyConfig, result.push({ gameItemId: 'buy-energy-id', name: 'Energy', - limit: energyConfig.energyBuyLimit, description: '', - price: energyConfig.energyPrice, - isEnergy: true + price: energyConfig.energyPrice }); } @@ -107,15 +105,24 @@ function Component ({ className, energyConfig, }); return result; - }, [gameId, gameInventoryItemList, gameItemMap]); + }, [energyConfig, gameId, gameInventoryItemList, gameItemMap]); - const onBuy = useCallback((gameItemId: number) => { + const onBuy = useCallback((gameItemId: string) => { setBuyLoading(true); - apiSDK.buyItem(gameItemId).catch((e) => { - console.log('buyItem error', e); - }).finally(() => { - setBuyLoading(false); - }); + + if (gameItemId === 'buy-energy-id') { + apiSDK.buyEnergy().catch((e) => { + console.log('buyEnergy error', e); + }).finally(() => { + setBuyLoading(false); + }); + } else { + apiSDK.buyItem(+gameItemId).catch((e) => { + console.log('buyItem error', e); + }).finally(() => { + setBuyLoading(false); + }); + } }, []); return ( diff --git a/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx index 699cf016349..d89d87885c2 100644 --- a/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx +++ b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx @@ -10,7 +10,7 @@ import React, { useCallback } from 'react'; import styled from 'styled-components'; type Props = ThemeProps & ShopItemInfo & { - onBuy: (gameItemId: number, quantity?: number) => void + onBuy: (gameItemId: string, quantity?: number) => void }; function Component (props: Props): React.ReactElement { diff --git a/packages/extension-koni-ui/src/connector/booka/sdk.ts b/packages/extension-koni-ui/src/connector/booka/sdk.ts index a10abbc8f18..095856711ad 100644 --- a/packages/extension-koni-ui/src/connector/booka/sdk.ts +++ b/packages/extension-koni-ui/src/connector/booka/sdk.ts @@ -417,6 +417,21 @@ export class BookaSdk { await this.reloadAccount(); } + async useInventoryItem (gameItemId: number) { + await this.postRequest(`${GAME_API_HOST}/api/shop/use-inventory-item`, { gameItemId }); + + await this.fetchGameInventoryItemList(); + + await this.fetchGameItemMap(); + + await this.reloadAccount(); + } + + async buyEnergy () { + await this.postRequest(`${GAME_API_HOST}/api/shop/buy-energy`, {}); + + await this.reloadAccount(); + } // --- shop async fetchLeaderboard () { diff --git a/packages/extension-koni-ui/src/types/shop.ts b/packages/extension-koni-ui/src/types/shop.ts index d2d9035d278..a8a53763f77 100644 --- a/packages/extension-koni-ui/src/types/shop.ts +++ b/packages/extension-koni-ui/src/types/shop.ts @@ -4,7 +4,6 @@ export type ShopItemInfo = { icon?: string; gameItemId: string; - isEnergy?: boolean; name: string; gameId?: number; limit?: number; @@ -14,4 +13,5 @@ export type ShopItemInfo = { itemGroupLevel?: number; price: number; disabled?: boolean; + usable?: boolean; } From cf3314ff272e574affc11ed27016df092ee035bc Mon Sep 17 00:00:00 2001 From: lw Date: Mon, 13 May 2024 18:54:46 +0700 Subject: [PATCH 09/13] [Shop] Update use item --- .../src/components/Modal/Shop/ShopModal.tsx | 24 ++++++++++++++----- .../src/components/Shop/ShopItem.tsx | 24 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx index 7685f479c41..cabd5ac7001 100644 --- a/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx +++ b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx @@ -25,7 +25,7 @@ function Component ({ className, energyConfig, gameId, gameInventoryItemList, gameItemMap }: Props): React.ReactElement { const { t } = useTranslation(); - const [buyLoading, setBuyLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const { inactiveModal } = useContext(ModalContext); @@ -65,7 +65,8 @@ function Component ({ className, energyConfig, itemGroup: gi.itemGroup, itemGroupLevel: gi.itemGroupLevel, price: gi.price, - disabled: disabled || (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1) + disabled: disabled || (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1), + usable: !!inventoryQuantity && inventoryQuantity > 0 && inventoryItemMapByGameItemId[gi.id]?.usable }; }; @@ -108,23 +109,33 @@ function Component ({ className, energyConfig, }, [energyConfig, gameId, gameInventoryItemList, gameItemMap]); const onBuy = useCallback((gameItemId: string) => { - setBuyLoading(true); + setIsLoading(true); if (gameItemId === 'buy-energy-id') { apiSDK.buyEnergy().catch((e) => { console.log('buyEnergy error', e); }).finally(() => { - setBuyLoading(false); + setIsLoading(false); }); } else { apiSDK.buyItem(+gameItemId).catch((e) => { console.log('buyItem error', e); }).finally(() => { - setBuyLoading(false); + setIsLoading(false); }); } }, []); + const onUse = useCallback((gameItemId: string) => { + setIsLoading(true); + + apiSDK.useInventoryItem(+gameItemId).catch((e) => { + console.log('onUse error', e); + }).finally(() => { + setIsLoading(false); + }); + }, []); + return ( )) } diff --git a/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx index d89d87885c2..5f58e09d1fc 100644 --- a/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx +++ b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; type Props = ThemeProps & ShopItemInfo & { onBuy: (gameItemId: string, quantity?: number) => void + onUse: (gameItemId: string) => void }; function Component (props: Props): React.ReactElement { @@ -20,12 +21,16 @@ function Component (props: Props): React.ReactElement { gameItemId, inventoryQuantity, limit, - name, onBuy, price } = props; + name, onBuy, price, usable, onUse } = props; const _onBuy = useCallback(() => { onBuy(gameItemId, 1); }, [gameItemId, onBuy]); + const _onUse = useCallback(() => { + onUse(gameItemId); + }, [gameItemId, onUse]); + return (
{
{name}
-
{description}
+
description: {description}
{ !!limit && (
Limit: {limit}
@@ -59,6 +64,16 @@ function Component (props: Props): React.ReactElement {
+ { + usable && ( + + ) + } +