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 d863d5ce72..9f2985b230 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 { 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'; -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,17 @@ 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 [energyConfig, setEnergyConfig] = useState(apiSDK.energyConfig); + 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 +77,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 +93,24 @@ const Component = ({ className }: Props): React.ReactElement => { setGameList(data); }); + const energyConfigSub = apiSDK.subscribeEnergyConfig().subscribe((data) => { + setEnergyConfig(data); + }); + + const gameItemMapSub = apiSDK.subscribeGameItemMap().subscribe((data) => { + setGameItemMap(data); + }); + + const gameInventoryItemListSub = apiSDK.subscribeGameInventoryItemList().subscribe((data) => { + setGameInventoryItemList(data); + }); + return () => { accountSub.unsubscribe(); + energyConfigSub.unsubscribe(); gameListSub.unsubscribe(); + gameItemMapSub.unsubscribe(); + gameInventoryItemListSub.unsubscribe(); }; }, []); @@ -92,8 +124,21 @@ const Component = ({ className }: Props): React.ReactElement => { /> + + +
+ +
+ { src={currentGame.url} /> } + + ; }; 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 0000000000..faee880606 --- /dev/null +++ b/packages/extension-koni-ui/src/Popup/Home/Mission/TaskCategoryList.tsx @@ -0,0 +1,103 @@ +// 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 { Icon, Image, Typography } from '@subwallet/react-ui'; +import { CaretRight } from 'phosphor-react'; +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-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 + } + }; +}); 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 c27808af43..16e14c7650 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 f24a1d8d2c..88d8ebe18c 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,138 @@ 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 { + result[t.categoryId].tasks.push(t); + + if (t.completedAt || t.status > 0) { + return; + } + + 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); + } + }); + + 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)); + }, 10000); }); 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/Games/GameEnergy.tsx b/packages/extension-koni-ui/src/components/Games/GameEnergy.tsx index a22770a6ab..4fb19448c7 100644 --- a/packages/extension-koni-ui/src/components/Games/GameEnergy.tsx +++ b/packages/extension-koni-ui/src/components/Games/GameEnergy.tsx @@ -10,14 +10,14 @@ type GameEnergyProps = ThemeProps & { className?: string; startTime: string; energy: number; + maxEnergy?: number; }; -const maxEnergy = 300; const ONE_SECOND = 1000; // 1 second in milliseconds const regenSeconds = 60; const regenTime = ONE_SECOND * regenSeconds; -function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) { +function _GameEnergy ({ className, energy, maxEnergy, startTime }: GameEnergyProps) { const [countdown, setCountdown] = useState(); 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
= { 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/components/Modal/Shop/ShopModal.tsx b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx new file mode 100644 index 0000000000..cabd5ac700 --- /dev/null +++ b/packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx @@ -0,0 +1,170 @@ +// 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 { EnergyConfig, 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; + energyConfig?: EnergyConfig; + gameItemMap: Record; + gameInventoryItemList: GameInventoryItem[]; +}; + +export const ShopModalId = 'ShopModalId'; +const apiSDK = BookaSdk.instance; + +function Component ({ className, energyConfig, + gameId, + gameInventoryItemList, gameItemMap }: Props): React.ReactElement { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + + const { inactiveModal } = useContext(ModalContext); + + const onClose = useCallback(() => { + inactiveModal(ShopModalId); + }, [inactiveModal]); + + const items = useMemo(() => { + const result: ShopItemInfo[] = []; + + if (energyConfig) { + result.push({ + gameItemId: 'buy-energy-id', + name: 'Energy', + description: '', + price: energyConfig.energyPrice + }); + } + + const inventoryItemMapByGameItemId: Record = {}; + + gameInventoryItemList.forEach((i) => { + inventoryItemMapByGameItemId[i.gameItemId] = i; + }); + + 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), + usable: !!inventoryQuantity && inventoryQuantity > 0 && inventoryItemMapByGameItemId[gi.id]?.usable + }; + }; + + [...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)) { + result.push(getShopItem(gi)); + } + }); + }); + + return result; + }, [energyConfig, gameId, gameInventoryItemList, gameItemMap]); + + const onBuy = useCallback((gameItemId: string) => { + setIsLoading(true); + + if (gameItemId === 'buy-energy-id') { + apiSDK.buyEnergy().catch((e) => { + console.log('buyEnergy error', e); + }).finally(() => { + setIsLoading(false); + }); + } else { + apiSDK.buyItem(+gameItemId).catch((e) => { + console.log('buyItem error', e); + }).finally(() => { + setIsLoading(false); + }); + } + }, []); + + const onUse = useCallback((gameItemId: string) => { + setIsLoading(true); + + apiSDK.useInventoryItem(+gameItemId).catch((e) => { + console.log('onUse error', e); + }).finally(() => { + setIsLoading(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 0000000000..aeaba09f14 --- /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 da360635ee..eb153e9170 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 0000000000..5f58e09d1f --- /dev/null +++ b/packages/extension-koni-ui/src/components/Shop/ShopItem.tsx @@ -0,0 +1,101 @@ +// 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: string, quantity?: number) => void + onUse: (gameItemId: string) => void +}; + +function Component (props: Props): React.ReactElement { + const { className = '', + description, + disabled, + gameItemId, + inventoryQuantity, + limit, + 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}
+ { + !!limit && ( +
Limit: {limit}
+ ) + } + +
Price: {price}
+ + { + !!inventoryQuantity && ( +
Quantity: {inventoryQuantity}
+ ) + } + +
+ + { + usable && ( + + ) + } + + +
+ ); +} + +const ShopItem = styled(Component)(({ theme: { token } }: Props) => { + return ({ + display: 'flex', + backgroundColor: token.colorBgSecondary, + padding: token.paddingSM, + borderRadius: token.borderRadiusLG, + gap: token.sizeXS, + + '.__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 0000000000..2632b841b4 --- /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 5b5a4a4303..57de0080d0 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 d24769d840..095856711a 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, 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'; @@ -16,21 +16,27 @@ 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' + gameList: 'data--game-list-cache', + energyConfig: 'data--energy-config' }; 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([]); + private gameItemMapSubject = new BehaviorSubject>({}); + private gameInventoryItemListSubject = new BehaviorSubject([]); + private energyConfigSubject = new BehaviorSubject(undefined); constructor () { - storage.getItems(Object.values(CACHE_KEYS)).then(([account, tasks, game]) => { + storage.getItems(Object.values(CACHE_KEYS)).then(([account, taskCategory, tasks, game, energyConfig]) => { if (account) { try { const accountData = JSON.parse(account) as BookaAccount; @@ -41,6 +47,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[]; @@ -60,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); } @@ -71,14 +97,30 @@ export class BookaSdk { return this.accountSubject.value; } + public get energyConfig () { + return this.energyConfigSubject.value; + } + public get taskList () { return this.taskListSubject.value; } + public get taskCategoryList () { + return this.taskCategoryListSubject.value; + } + public get gameList () { 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; } @@ -146,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`); @@ -159,6 +214,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 +245,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 +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.fetchTaskList(), this.fetchLeaderboard()]); + 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'); } @@ -298,6 +377,63 @@ 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(); + } + + 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 () { 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 58d61df8ac..a0bad5ac7c 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; @@ -27,23 +67,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 bbb3023ca4..df0342c1dd 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'; 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 0000000000..a8a53763f7 --- /dev/null +++ b/packages/extension-koni-ui/src/types/shop.ts @@ -0,0 +1,17 @@ +// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export type ShopItemInfo = { + icon?: string; + gameItemId: string; + name: string; + gameId?: number; + limit?: number; + description?: string; + inventoryQuantity?: number; + itemGroup?: string; + itemGroupLevel?: number; + price: number; + disabled?: boolean; + usable?: boolean; +}