feat: 联调充值和提现接口
This commit is contained in:
209
src/features/game/hooks/use-animal-vm.ts
Normal file
209
src/features/game/hooks/use-animal-vm.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
||||
import {
|
||||
selectSelectionTotal,
|
||||
useGameRoundStore,
|
||||
useGameSessionStore,
|
||||
} from '@/store/game'
|
||||
|
||||
function parseBalance(value: string | number | null | undefined) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return 0
|
||||
}
|
||||
|
||||
const parsed = Number(value)
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
export type DesktopAnimalWarningType = 'balance' | 'limit'
|
||||
|
||||
function getNextMarqueeId(ids: number[], currentId: number | null) {
|
||||
if (ids.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (ids.length === 1) {
|
||||
return ids[0] ?? null
|
||||
}
|
||||
|
||||
let nextId = currentId
|
||||
|
||||
while (nextId === currentId) {
|
||||
nextId = ids[Math.floor(Math.random() * ids.length)] ?? currentId
|
||||
}
|
||||
|
||||
return nextId
|
||||
}
|
||||
|
||||
export function useAnimalVm(
|
||||
animalIds: number[],
|
||||
onSelect?: (animalId: number) => void,
|
||||
) {
|
||||
const { t } = useTranslation()
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const markSoundPlaybackUnlocked = useAudioStore(
|
||||
(state) => state.markSoundPlaybackUnlocked,
|
||||
)
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||
const chips = useGameRoundStore((state) => state.chips)
|
||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||
const maxSelectionCount = useGameRoundStore(
|
||||
(state) => state.maxSelectionCount,
|
||||
)
|
||||
const placeBet = useGameRoundStore((state) => state.placeBet)
|
||||
const removeSelectionsForCell = useGameRoundStore(
|
||||
(state) => state.removeSelectionsForCell,
|
||||
)
|
||||
const selections = useGameRoundStore((state) => state.selections)
|
||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||
const connection = useGameSessionStore((state) => state.connection)
|
||||
const requestRealtimeConnection = useGameSessionStore(
|
||||
(state) => state.requestRealtimeConnection,
|
||||
)
|
||||
const shouldConnectRealtime = useGameSessionStore(
|
||||
(state) => state.shouldConnectRealtime,
|
||||
)
|
||||
const [marqueeId, setMarqueeId] = useState<number | null>(() =>
|
||||
getNextMarqueeId(animalIds, null),
|
||||
)
|
||||
const [cellWarning, setCellWarning] = useState<{
|
||||
cellId: number
|
||||
type: DesktopAnimalWarningType
|
||||
} | null>(null)
|
||||
|
||||
const activeChip = useMemo(
|
||||
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
||||
[activeChipId, chips],
|
||||
)
|
||||
const balance = parseBalance(currentUser?.coin)
|
||||
const selectionByCell = useMemo(() => {
|
||||
return selections.reduce<Record<number, { amount: number; count: number }>>(
|
||||
(accumulator, selection) => {
|
||||
const current = accumulator[selection.cellId] ?? { amount: 0, count: 0 }
|
||||
|
||||
accumulator[selection.cellId] = {
|
||||
amount: current.amount + selection.amount,
|
||||
count: current.count + 1,
|
||||
}
|
||||
|
||||
return accumulator
|
||||
},
|
||||
{},
|
||||
)
|
||||
}, [selections])
|
||||
|
||||
const isRealtimeConnected = connection.status === 'connected'
|
||||
const isRealtimeConnecting =
|
||||
shouldConnectRealtime &&
|
||||
(connection.status === 'connecting' || connection.status === 'reconnecting')
|
||||
const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected
|
||||
const lockInteraction = showStandbyState
|
||||
const selectedCellCount = Object.keys(selectionByCell).length
|
||||
|
||||
useEffect(() => {
|
||||
if (cellWarning === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
setCellWarning((currentWarning) =>
|
||||
currentWarning?.cellId === cellWarning.cellId &&
|
||||
currentWarning.type === cellWarning.type
|
||||
? null
|
||||
: currentWarning,
|
||||
)
|
||||
}, 1200)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [cellWarning])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showStandbyState) {
|
||||
setMarqueeId(null)
|
||||
return
|
||||
}
|
||||
|
||||
setMarqueeId((currentId) => getNextMarqueeId(animalIds, currentId))
|
||||
|
||||
let timerId = 0
|
||||
|
||||
const loop = () => {
|
||||
setMarqueeId((currentId) => getNextMarqueeId(animalIds, currentId))
|
||||
timerId = window.setTimeout(loop, 180 + Math.floor(Math.random() * 220))
|
||||
}
|
||||
|
||||
timerId = window.setTimeout(loop, 220)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [animalIds, showStandbyState])
|
||||
|
||||
const handleStart = () => {
|
||||
if (authStatus !== 'authenticated') {
|
||||
notify.warning(t('commonUi.toast.loginRequired'))
|
||||
setModalOpen('desktopLogin', true)
|
||||
return
|
||||
}
|
||||
|
||||
clearSelections()
|
||||
markSoundPlaybackUnlocked()
|
||||
requestRealtimeConnection()
|
||||
}
|
||||
|
||||
const handleSelect = (animalId: number) => {
|
||||
if (showStandbyState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (onSelect) {
|
||||
onSelect(animalId)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectionByCell[animalId]) {
|
||||
removeSelectionsForCell(animalId)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedCellCount >= maxSelectionCount) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'limit',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (totalBetAmount + (activeChip?.amount ?? 0) > balance) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'balance',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
placeBet(animalId)
|
||||
}
|
||||
|
||||
return {
|
||||
cellWarning,
|
||||
handleSelect,
|
||||
handleStart,
|
||||
isRealtimeConnecting,
|
||||
lockInteraction,
|
||||
marqueeId,
|
||||
selectionByCell,
|
||||
showStandbyState,
|
||||
}
|
||||
}
|
||||
15
src/features/game/hooks/use-deposit-tier-list.ts
Normal file
15
src/features/game/hooks/use-deposit-tier-list.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getDepositTierList } from '@/features/game/api'
|
||||
|
||||
export function useDepositTierList() {
|
||||
const { i18n } = useTranslation()
|
||||
const language = i18n.resolvedLanguage ?? i18n.language ?? 'zh-CN'
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['finance', 'deposit-tier-list', language],
|
||||
queryFn: () => getDepositTierList(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
15
src/features/game/hooks/use-deposit-withdraw-config.ts
Normal file
15
src/features/game/hooks/use-deposit-withdraw-config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getDepositWithdrawConfig } from '@/features/game/api'
|
||||
|
||||
export function useDepositWithdrawConfig() {
|
||||
const { i18n } = useTranslation()
|
||||
const language = i18n.resolvedLanguage ?? i18n.language ?? 'zh-CN'
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['finance', 'deposit-withdraw-config', language],
|
||||
queryFn: () => getDepositWithdrawConfig(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
@@ -175,7 +175,6 @@ export function useGameControlVm() {
|
||||
|
||||
try {
|
||||
let latestBalance = currentUser?.coin ?? '0'
|
||||
let latestStreak = currentUser?.currentStreak ?? 0
|
||||
|
||||
for (const group of groupedSelections.values()) {
|
||||
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
||||
@@ -193,14 +192,12 @@ export function useGameControlVm() {
|
||||
}
|
||||
|
||||
latestBalance = result.balance_after
|
||||
latestStreak = result.current_streak
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
setCurrentUser({
|
||||
...currentUser,
|
||||
coin: latestBalance,
|
||||
currentStreak: latestStreak,
|
||||
lastBetPeriodNo: round.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ function formatNumbers(numbers: number[]) {
|
||||
return numbers.map((number) => String(number).padStart(2, '0')).join(', ')
|
||||
}
|
||||
|
||||
type HistoryResultState = 'lost' | 'pending' | 'win'
|
||||
|
||||
export function useGameHistoryVm() {
|
||||
const { i18n, t } = useTranslation()
|
||||
const accessToken = useAuthStore((state) => state.accessToken)
|
||||
@@ -69,9 +71,12 @@ export function useGameHistoryVm() {
|
||||
i18n.resolvedLanguage ?? 'en-US',
|
||||
),
|
||||
id: entry.order_no,
|
||||
isWin:
|
||||
entry.result_number !== null &&
|
||||
entry.numbers.includes(entry.result_number),
|
||||
resultState:
|
||||
entry.result_number === null
|
||||
? ('pending' satisfies HistoryResultState)
|
||||
: entry.numbers.includes(entry.result_number)
|
||||
? ('win' satisfies HistoryResultState)
|
||||
: ('lost' satisfies HistoryResultState),
|
||||
numbersLabel: formatNumbers(entry.numbers),
|
||||
numbers: entry.numbers,
|
||||
orderNo: entry.order_no,
|
||||
|
||||
@@ -8,7 +8,13 @@ import {
|
||||
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
|
||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||
import { getGameLobbyInit, normalizePeriodTickRound } from '../api/game-api'
|
||||
import type { GameLobbyUserSnapshotDto, GamePeriodTickDto } from '../api/types'
|
||||
import type { GamePeriodTickDto } from '../api/types'
|
||||
|
||||
type UserStreakMessageData = {
|
||||
currentStreak: number
|
||||
oddsFactor?: number
|
||||
streakLevel?: number
|
||||
}
|
||||
|
||||
const FALLBACK_POLL_INTERVAL_MS = 10_000
|
||||
const GAME_SOCKET_TOPICS = {
|
||||
@@ -36,6 +42,10 @@ const GAME_SOCKET_TOPICS = {
|
||||
adminLiveOpened: 'admin.live.opened',
|
||||
} as const
|
||||
|
||||
const GAME_SOCKET_TOPIC_VALUES = new Set<string>(
|
||||
Object.values(GAME_SOCKET_TOPICS),
|
||||
)
|
||||
|
||||
// 当前 H5 游戏页实际需要的用户侧事件。
|
||||
// 后台专用事件保持在 GAME_SOCKET_TOPICS 中做口径对齐,但不在这里订阅。
|
||||
const PLAYER_SOCKET_TOPICS = [
|
||||
@@ -93,6 +103,22 @@ function getNestedRecord(
|
||||
: null
|
||||
}
|
||||
|
||||
function getMessageTopic(message: GameSocketMessage) {
|
||||
const root = message as Record<string, unknown>
|
||||
const event = typeof root.event === 'string' ? root.event : null
|
||||
const topic = typeof root.topic === 'string' ? root.topic : null
|
||||
|
||||
if (event && GAME_SOCKET_TOPIC_VALUES.has(event)) {
|
||||
return event
|
||||
}
|
||||
|
||||
if (topic && GAME_SOCKET_TOPIC_VALUES.has(topic)) {
|
||||
return topic
|
||||
}
|
||||
|
||||
return event ?? topic
|
||||
}
|
||||
|
||||
function extractServerTime(message: GameSocketMessage) {
|
||||
const root = message as Record<string, unknown>
|
||||
|
||||
@@ -105,31 +131,22 @@ function extractServerTime(message: GameSocketMessage) {
|
||||
return typeof data?.server_time === 'number' ? data.server_time : null
|
||||
}
|
||||
|
||||
function extractUserSnapshot(
|
||||
function extractUserStreakMessageData(
|
||||
message: GameSocketMessage,
|
||||
): GameLobbyUserSnapshotDto | null {
|
||||
): UserStreakMessageData | null {
|
||||
const direct = getNestedRecord(message, 'user_snapshot')
|
||||
const nested = getNestedRecord(
|
||||
getNestedRecord(message, 'data'),
|
||||
'user_snapshot',
|
||||
)
|
||||
const source = direct ?? nested
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const nested = getNestedRecord(data, 'user_snapshot')
|
||||
const source = direct ?? nested ?? data
|
||||
|
||||
if (
|
||||
!source ||
|
||||
typeof source.coin !== 'string' ||
|
||||
typeof source.current_streak !== 'number'
|
||||
) {
|
||||
if (!source || typeof source.current_streak !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
coin: source.coin,
|
||||
current_streak: source.current_streak,
|
||||
is_jackpot:
|
||||
typeof source.is_jackpot === 'boolean' ? source.is_jackpot : undefined,
|
||||
odds_factor: toOptionalNumber(source.odds_factor),
|
||||
streak_level: toOptionalNumber(source.streak_level),
|
||||
currentStreak: source.current_streak,
|
||||
oddsFactor: toOptionalNumber(source.odds_factor),
|
||||
streakLevel: toOptionalNumber(source.streak_level),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +186,25 @@ function extractPeriodTick(
|
||||
}
|
||||
}
|
||||
|
||||
function extractWalletCoin(message: GameSocketMessage) {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const source = data ?? (message as Record<string, unknown>)
|
||||
const coin = source.coin ?? source.balance ?? source.balance_after
|
||||
|
||||
if (typeof coin === 'string') {
|
||||
return coin
|
||||
}
|
||||
|
||||
return typeof coin === 'number' && Number.isFinite(coin) ? String(coin) : null
|
||||
}
|
||||
|
||||
function extractJackpotStatus(message: GameSocketMessage) {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const source = data ?? (message as Record<string, unknown>)
|
||||
|
||||
return typeof source.is_jackpot === 'boolean' ? source.is_jackpot : true
|
||||
}
|
||||
|
||||
function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
|
||||
const currentRoundState = useGameRoundStore.getState()
|
||||
const currentSessionState = useGameSessionStore.getState()
|
||||
@@ -211,52 +247,122 @@ function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyRealtimeMessage(message: GameSocketMessage) {
|
||||
const serverTime = extractServerTime(message)
|
||||
function applyPeriodMessage(
|
||||
message: GameSocketMessage,
|
||||
serverTime: number | null,
|
||||
) {
|
||||
const period = extractPeriodTick(message)
|
||||
const userSnapshot = extractUserSnapshot(message)
|
||||
|
||||
if (period) {
|
||||
const previousRound = useGameRoundStore.getState().round
|
||||
const round = normalizePeriodTickRound(
|
||||
{
|
||||
...period,
|
||||
server_time: serverTime ?? period.server_time,
|
||||
},
|
||||
previousRound,
|
||||
)
|
||||
|
||||
useGameRoundStore.getState().syncRound({
|
||||
bettingClosesAt: round.bettingClosesAt,
|
||||
id: round.id,
|
||||
phase: round.phase,
|
||||
revealingAt: round.revealingAt,
|
||||
settledAt: round.settledAt,
|
||||
startedAt: round.startedAt,
|
||||
winningCellId: round.winningCellId,
|
||||
})
|
||||
useGameSessionStore.getState().syncDashboard({
|
||||
countdownMs: period.countdown * 1000,
|
||||
updatedAt:
|
||||
serverTime !== null
|
||||
? toIsoFromUnixSeconds(serverTime)
|
||||
: toIsoFromUnixSeconds(period.server_time),
|
||||
})
|
||||
if (!period) {
|
||||
return
|
||||
}
|
||||
|
||||
if (userSnapshot) {
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
const previousRound = useGameRoundStore.getState().round
|
||||
const round = normalizePeriodTickRound(
|
||||
{
|
||||
...period,
|
||||
server_time: serverTime ?? period.server_time,
|
||||
},
|
||||
previousRound,
|
||||
)
|
||||
|
||||
if (currentUser) {
|
||||
useAuthStore.getState().setCurrentUser({
|
||||
...currentUser,
|
||||
coin: userSnapshot.coin,
|
||||
currentStreak: userSnapshot.current_streak,
|
||||
isJackpot: userSnapshot.is_jackpot,
|
||||
oddsFactor: userSnapshot.odds_factor,
|
||||
streakLevel: userSnapshot.streak_level,
|
||||
})
|
||||
}
|
||||
useGameRoundStore.getState().syncRound({
|
||||
bettingClosesAt: round.bettingClosesAt,
|
||||
id: round.id,
|
||||
phase: round.phase,
|
||||
revealingAt: round.revealingAt,
|
||||
settledAt: round.settledAt,
|
||||
startedAt: round.startedAt,
|
||||
winningCellId: round.winningCellId,
|
||||
})
|
||||
useGameSessionStore.getState().syncDashboard({
|
||||
countdownMs: period.countdown * 1000,
|
||||
updatedAt:
|
||||
serverTime !== null
|
||||
? toIsoFromUnixSeconds(serverTime)
|
||||
: toIsoFromUnixSeconds(period.server_time),
|
||||
})
|
||||
}
|
||||
|
||||
function applyPeriodPhase(phase: 'locked' | 'revealing' | 'settled') {
|
||||
useGameRoundStore.getState().setPhase(phase)
|
||||
}
|
||||
|
||||
function applyUserStreakMessage(message: GameSocketMessage) {
|
||||
const streakData = extractUserStreakMessageData(message)
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
|
||||
if (!streakData || !currentUser) {
|
||||
return
|
||||
}
|
||||
|
||||
useAuthStore.getState().setCurrentUser({
|
||||
...currentUser,
|
||||
currentStreak: streakData.currentStreak,
|
||||
oddsFactor: streakData.oddsFactor,
|
||||
streakLevel: streakData.streakLevel,
|
||||
})
|
||||
}
|
||||
|
||||
function applyWalletChangedMessage(message: GameSocketMessage) {
|
||||
const coin = extractWalletCoin(message)
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
|
||||
if (coin === null || !currentUser) {
|
||||
return
|
||||
}
|
||||
|
||||
useAuthStore.getState().setCurrentUser({
|
||||
...currentUser,
|
||||
coin,
|
||||
})
|
||||
}
|
||||
|
||||
function applyJackpotHitMessage(message: GameSocketMessage) {
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
|
||||
if (!currentUser) {
|
||||
return
|
||||
}
|
||||
|
||||
useAuthStore.getState().setCurrentUser({
|
||||
...currentUser,
|
||||
isJackpot: extractJackpotStatus(message),
|
||||
})
|
||||
}
|
||||
|
||||
function applyRealtimeMessage(message: GameSocketMessage) {
|
||||
const serverTime = extractServerTime(message)
|
||||
const topic = getMessageTopic(message)
|
||||
|
||||
switch (topic) {
|
||||
case GAME_SOCKET_TOPICS.periodTick:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.periodLocked:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('locked')
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.periodOpened:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('revealing')
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.periodPayout:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('settled')
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.userStreak:
|
||||
applyUserStreakMessage(message)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.walletChanged:
|
||||
applyWalletChangedMessage(message)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.jackpotHit:
|
||||
applyJackpotHitMessage(message)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.betAccepted:
|
||||
case GAME_SOCKET_TOPICS.autoSpinProgress:
|
||||
break
|
||||
}
|
||||
|
||||
useGameSessionStore.getState().syncConnection({
|
||||
|
||||
244
src/features/game/hooks/use-header-vm.ts
Normal file
244
src/features/game/hooks/use-header-vm.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useAppLanguage } from '@/features/game/hooks/use-app-language'
|
||||
import {
|
||||
isDesktopFullscreen,
|
||||
subscribeDesktopFullscreenChange,
|
||||
toggleDesktopFullscreen,
|
||||
} from '@/lib/utils'
|
||||
import {
|
||||
useAudioStore,
|
||||
useAuthStore,
|
||||
useGameSessionStore,
|
||||
useModalStore,
|
||||
} from '@/store'
|
||||
|
||||
type BrowserNetworkInformation = {
|
||||
addEventListener?: (type: 'change', listener: () => void) => void
|
||||
downlink?: number
|
||||
effectiveType?: string
|
||||
removeEventListener?: (type: 'change', listener: () => void) => void
|
||||
rtt?: number
|
||||
}
|
||||
|
||||
type SignalPresentation = {
|
||||
activeBars: number
|
||||
latencyLabel: string
|
||||
toneClassName: string
|
||||
}
|
||||
|
||||
function formatTimezoneOffset(date: Date) {
|
||||
const offsetMinutes = -date.getTimezoneOffset()
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-'
|
||||
const absoluteMinutes = Math.abs(offsetMinutes)
|
||||
const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, '0')
|
||||
const minutes = String(absoluteMinutes % 60).padStart(2, '0')
|
||||
|
||||
return `GMT${sign}${hours}${minutes === '00' ? '' : `:${minutes}`}`
|
||||
}
|
||||
|
||||
function formatHeaderTime(date: Date) {
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${hours}:${minutes}:${seconds} ${formatTimezoneOffset(date)}`
|
||||
}
|
||||
|
||||
function getBrowserNetworkInformation() {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (navigator as Navigator & { connection?: BrowserNetworkInformation })
|
||||
.connection
|
||||
}
|
||||
|
||||
function resolveSignalPresentation(input: {
|
||||
isOnline: boolean
|
||||
latencyMs: number | null
|
||||
status: string
|
||||
}) {
|
||||
if (!input.isOnline || input.status === 'disconnected') {
|
||||
return {
|
||||
activeBars: 0,
|
||||
latencyLabel: '--',
|
||||
toneClassName: 'text-[#FF6B6B]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
if (input.latencyMs === null) {
|
||||
return {
|
||||
activeBars: input.status === 'connected' ? 2 : 1,
|
||||
latencyLabel: '--',
|
||||
toneClassName: 'text-[#7F8EA3]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
if (input.latencyMs <= 80) {
|
||||
return {
|
||||
activeBars: 4,
|
||||
latencyLabel: String(input.latencyMs),
|
||||
toneClassName: 'text-[#74FF69]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
if (input.latencyMs <= 150) {
|
||||
return {
|
||||
activeBars: 3,
|
||||
latencyLabel: String(input.latencyMs),
|
||||
toneClassName: 'text-[#B7FF6A]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
if (input.latencyMs <= 300) {
|
||||
return {
|
||||
activeBars: 2,
|
||||
latencyLabel: String(input.latencyMs),
|
||||
toneClassName: 'text-[#FFD76A]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
return {
|
||||
activeBars: 1,
|
||||
latencyLabel: String(input.latencyMs),
|
||||
toneClassName: 'text-[#FF8A6A]',
|
||||
} satisfies SignalPresentation
|
||||
}
|
||||
|
||||
export function useHeaderVm() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [clockNow, setClockNow] = useState(() => Date.now())
|
||||
const [isOnline, setIsOnline] = useState(() =>
|
||||
typeof navigator === 'undefined' ? true : navigator.onLine,
|
||||
)
|
||||
const [browserNetworkRttMs, setBrowserNetworkRttMs] = useState<number | null>(
|
||||
() => {
|
||||
const rtt = getBrowserNetworkInformation()?.rtt
|
||||
|
||||
return typeof rtt === 'number' && Number.isFinite(rtt) && rtt > 0
|
||||
? rtt
|
||||
: null
|
||||
},
|
||||
)
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const isSoundEnabled = useAudioStore((state) => state.isSoundEnabled)
|
||||
const toggleSoundEnabled = useAudioStore((state) => state.toggleSoundEnabled)
|
||||
const connection = useGameSessionStore((state) => state.connection)
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
||||
|
||||
const serverClockOffsetMs = useMemo(() => {
|
||||
if (
|
||||
connection.status !== 'connected' ||
|
||||
connection.transport !== 'websocket' ||
|
||||
!connection.lastMessageAt
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const serverTimestamp = Date.parse(connection.lastMessageAt)
|
||||
|
||||
if (Number.isNaN(serverTimestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return serverTimestamp - Date.now()
|
||||
}, [connection.lastMessageAt, connection.status, connection.transport])
|
||||
|
||||
const systemTimeLabel = useMemo(() => {
|
||||
const activeTimestamp =
|
||||
serverClockOffsetMs === null ? clockNow : clockNow + serverClockOffsetMs
|
||||
|
||||
return formatHeaderTime(new Date(activeTimestamp))
|
||||
}, [clockNow, serverClockOffsetMs])
|
||||
|
||||
const signalLatencyMs = useMemo(() => {
|
||||
if (
|
||||
typeof connection.latencyMs === 'number' &&
|
||||
Number.isFinite(connection.latencyMs) &&
|
||||
connection.latencyMs >= 0
|
||||
) {
|
||||
return connection.latencyMs
|
||||
}
|
||||
|
||||
return browserNetworkRttMs
|
||||
}, [browserNetworkRttMs, connection.latencyMs])
|
||||
|
||||
const signalPresentation = useMemo(
|
||||
() =>
|
||||
resolveSignalPresentation({
|
||||
isOnline,
|
||||
latencyMs: signalLatencyMs,
|
||||
status: connection.status,
|
||||
}),
|
||||
[connection.status, isOnline, signalLatencyMs],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const syncFullscreenState = () => {
|
||||
setIsFullscreen(isDesktopFullscreen())
|
||||
}
|
||||
|
||||
syncFullscreenState()
|
||||
return subscribeDesktopFullscreenChange(syncFullscreenState)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
setClockNow(Date.now())
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const syncBrowserNetworkState = () => {
|
||||
setIsOnline(navigator.onLine)
|
||||
|
||||
const rtt = getBrowserNetworkInformation()?.rtt
|
||||
|
||||
setBrowserNetworkRttMs(
|
||||
typeof rtt === 'number' && Number.isFinite(rtt) && rtt > 0 ? rtt : null,
|
||||
)
|
||||
}
|
||||
|
||||
const networkInformation = getBrowserNetworkInformation()
|
||||
|
||||
syncBrowserNetworkState()
|
||||
window.addEventListener('online', syncBrowserNetworkState)
|
||||
window.addEventListener('offline', syncBrowserNetworkState)
|
||||
networkInformation?.addEventListener?.('change', syncBrowserNetworkState)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', syncBrowserNetworkState)
|
||||
window.removeEventListener('offline', syncBrowserNetworkState)
|
||||
networkInformation?.removeEventListener?.(
|
||||
'change',
|
||||
syncBrowserNetworkState,
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
authStatus,
|
||||
currentLanguageLabel,
|
||||
currentLanguageOption,
|
||||
currentUser,
|
||||
handleFullscreenToggle: () => toggleDesktopFullscreen(),
|
||||
isFullscreen,
|
||||
isSoundEnabled,
|
||||
onOpenLanguage: () => setModalOpen('desktopLanguage', true),
|
||||
onOpenLogin: () => setModalOpen('desktopLogin', true),
|
||||
onOpenNotice: () => setModalOpen('desktopNotice', true),
|
||||
onOpenProcedures: () => setModalOpen('desktopProcedures', true),
|
||||
onOpenRegister: () => setModalOpen('desktopRegister', true),
|
||||
onOpenRules: () => setModalOpen('desktopRules', true),
|
||||
onOpenUserInfo: () => setModalOpen('desktopUserInfo', true),
|
||||
signalPresentation,
|
||||
systemTimeLabel,
|
||||
toggleSoundEnabled,
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useAppPreferenceStore, useModalStore } from '@/store'
|
||||
|
||||
export function useProtocolAgreement() {
|
||||
const isHydrated = useAppPreferenceStore((state) => state.isHydrated)
|
||||
const hasAcceptedProtocol = useAppPreferenceStore(
|
||||
(state) => state.hasAcceptedProtocol,
|
||||
)
|
||||
const setProtocolAccepted = useAppPreferenceStore(
|
||||
(state) => state.setProtocolAccepted,
|
||||
)
|
||||
const open = useModalStore((state) => state.modals.desktopProtocol)
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHydrated) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasAcceptedProtocol) {
|
||||
setModalOpen('desktopProtocol', true)
|
||||
return
|
||||
}
|
||||
|
||||
setModalOpen('desktopProtocol', false)
|
||||
}, [hasAcceptedProtocol, isHydrated, setModalOpen])
|
||||
|
||||
const acceptProtocol = () => {
|
||||
setProtocolAccepted(true)
|
||||
setModalOpen('desktopProtocol', false)
|
||||
}
|
||||
|
||||
return {
|
||||
acceptProtocol,
|
||||
hasAcceptedProtocol,
|
||||
isHydrated,
|
||||
open,
|
||||
}
|
||||
}
|
||||
3
src/features/game/hooks/use-topup-vm.ts
Normal file
3
src/features/game/hooks/use-topup-vm.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function useTopupVm() {
|
||||
return {}
|
||||
}
|
||||
48
src/features/game/hooks/use-withdraw-submit.ts
Normal file
48
src/features/game/hooks/use-withdraw-submit.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
createWithdraw,
|
||||
type WithdrawCreateRequestDto,
|
||||
} from '@/features/game/api'
|
||||
import { notify } from '@/lib/notify'
|
||||
|
||||
export function useWithdrawSubmit() {
|
||||
const { i18n, t } = useTranslation()
|
||||
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: WithdrawCreateRequestDto) => createWithdraw(payload),
|
||||
onError: (error) => {
|
||||
notify.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('commonUi.toast.requestFailed'),
|
||||
)
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const formatter = new Intl.NumberFormat(locale, {
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
|
||||
notify.success(t('gameDesktop.withdraw.submitSuccess'), {
|
||||
description: [
|
||||
t('gameDesktop.withdraw.success.orderNo', {
|
||||
orderNo: data.order_no,
|
||||
}),
|
||||
t('gameDesktop.withdraw.success.actualArrivalCoin', {
|
||||
amount: formatter.format(data.actual_arrival_coin),
|
||||
}),
|
||||
t('gameDesktop.withdraw.success.feeCoin', {
|
||||
amount: formatter.format(data.fee_coin),
|
||||
}),
|
||||
t('gameDesktop.withdraw.success.reviewRequired', {
|
||||
value: data.risk_review_required
|
||||
? t('commonUi.dialog.yes')
|
||||
: t('commonUi.dialog.no'),
|
||||
}),
|
||||
].join('\n'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
339
src/features/game/hooks/use-withdraw-vm.ts
Normal file
339
src/features/game/hooks/use-withdraw-vm.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { DepositWithdrawConfig } from '@/features/game/api'
|
||||
import { useDepositWithdrawConfig } from '@/features/game/hooks/use-deposit-withdraw-config'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
const QUICK_FIAT_AMOUNTS = [3, 30, 50, 100, 200, 500] as const
|
||||
|
||||
const DEFAULT_WITHDRAW_CONFIG: DepositWithdrawConfig = {
|
||||
currencies: [
|
||||
{
|
||||
code: 'MYR',
|
||||
depositCoinsPerFiat: '100',
|
||||
depositCoinsPerFiatValue: 100,
|
||||
label: 'MYR',
|
||||
withdrawCoinsPerFiat: '100',
|
||||
withdrawCoinsPerFiatValue: 100,
|
||||
},
|
||||
],
|
||||
payChannels: [],
|
||||
platformCoinLabel: '钻石',
|
||||
rates: [
|
||||
{
|
||||
currency: 'MYR',
|
||||
diamondsPerFiatUnit: '100',
|
||||
diamondsPerFiatUnitValue: 100,
|
||||
},
|
||||
],
|
||||
withdraw: {
|
||||
banks: [],
|
||||
feeNote: 'RM10 - RM99.99 之间的交易将收取最低RM 1的提现手续费',
|
||||
minBank: '10',
|
||||
minEwallet: '10',
|
||||
processingNote: '30s即可到账',
|
||||
rateHint: '汇率为参考价格,实际以提现时为准。',
|
||||
rateMode: 'fixed' as const,
|
||||
},
|
||||
}
|
||||
|
||||
function formatNumber(locale: string, value: number) {
|
||||
return new Intl.NumberFormat(locale).format(value)
|
||||
}
|
||||
|
||||
function getInitialWithdrawAmount(
|
||||
selectedRate: number,
|
||||
maxWithdrawAmount: number,
|
||||
) {
|
||||
if (maxWithdrawAmount <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
maxWithdrawAmount,
|
||||
Math.max(1, Math.round(selectedRate * QUICK_FIAT_AMOUNTS[0])),
|
||||
)
|
||||
}
|
||||
|
||||
function getActiveCurrencyCode(
|
||||
currencies: DepositWithdrawConfig['currencies'],
|
||||
selectedCurrencyCode: string,
|
||||
) {
|
||||
return (
|
||||
currencies.find((item) => item.code === selectedCurrencyCode) ??
|
||||
currencies[0] ??
|
||||
DEFAULT_WITHDRAW_CONFIG.currencies[0]
|
||||
)
|
||||
}
|
||||
|
||||
function getNormalizedConfig(
|
||||
config: DepositWithdrawConfig | undefined,
|
||||
fallback: DepositWithdrawConfig,
|
||||
) {
|
||||
return config ?? fallback
|
||||
}
|
||||
|
||||
function isValidEmail(value: string) {
|
||||
if (value.trim().length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())
|
||||
}
|
||||
|
||||
function isValidPhone(value: string) {
|
||||
const normalized = value.replace(/[^\d+]/g, '')
|
||||
|
||||
if (normalized.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return /^\+?\d{6,20}$/.test(normalized)
|
||||
}
|
||||
|
||||
export function useWithdrawVm() {
|
||||
const { i18n, t } = useTranslation()
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const withdrawConfigQuery = useDepositWithdrawConfig()
|
||||
const config = useMemo(() => {
|
||||
const baseConfig = getNormalizedConfig(
|
||||
withdrawConfigQuery.data,
|
||||
DEFAULT_WITHDRAW_CONFIG,
|
||||
)
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
currencies:
|
||||
baseConfig.currencies.length > 0
|
||||
? baseConfig.currencies
|
||||
: DEFAULT_WITHDRAW_CONFIG.currencies,
|
||||
payChannels: baseConfig.payChannels,
|
||||
withdraw: {
|
||||
...baseConfig.withdraw,
|
||||
banks: baseConfig.withdraw.banks,
|
||||
},
|
||||
}
|
||||
}, [withdrawConfigQuery.data])
|
||||
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
|
||||
|
||||
const [amount, setAmountState] = useState(0)
|
||||
const [hasInitializedAmount, setHasInitializedAmount] = useState(false)
|
||||
const [currencyCode, setCurrencyCode] = useState(
|
||||
config.currencies[0]?.code ?? 'MYR',
|
||||
)
|
||||
const [paymentChannelCode, setPaymentChannelCode] = useState('')
|
||||
const [bankCode, setBankCode] = useState('')
|
||||
const [holderName, setHolderName] = useState('')
|
||||
const [bankAccount, setBankAccount] = useState('')
|
||||
const [receiverEmail, setReceiverEmail] = useState('')
|
||||
const [receiverPhone, setReceiverPhone] = useState('')
|
||||
|
||||
const selectedCurrency = getActiveCurrencyCode(
|
||||
config.currencies,
|
||||
currencyCode,
|
||||
)
|
||||
const selectedRate = selectedCurrency.withdrawCoinsPerFiatValue || 1
|
||||
const sortedPayChannels = useMemo(
|
||||
() =>
|
||||
[...config.payChannels]
|
||||
.filter((channel) => channel.status === 1)
|
||||
.sort((left, right) => left.sort - right.sort),
|
||||
[config.payChannels],
|
||||
)
|
||||
const sortedBanks = useMemo(
|
||||
() =>
|
||||
[...config.withdraw.banks]
|
||||
.filter((bank) => bank.status === 1)
|
||||
.sort((left, right) => left.sort - right.sort),
|
||||
[config.withdraw.banks],
|
||||
)
|
||||
const availableBalance = Number(currentUser?.coin ?? 0)
|
||||
const maxWithdrawAmount = Math.max(0, Math.floor(availableBalance))
|
||||
const selectedPaymentChannel =
|
||||
sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ??
|
||||
null
|
||||
const setAmount = useCallback(
|
||||
(nextAmount: number) => {
|
||||
setAmountState(
|
||||
Math.min(maxWithdrawAmount, Math.max(0, Math.floor(nextAmount))),
|
||||
)
|
||||
},
|
||||
[maxWithdrawAmount],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedCurrency &&
|
||||
selectedCurrency.code !== currencyCode &&
|
||||
config.currencies.some((item) => item.code === currencyCode)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedCurrency && selectedCurrency.code !== currencyCode) {
|
||||
setCurrencyCode(selectedCurrency.code)
|
||||
}
|
||||
}, [config.currencies, currencyCode, selectedCurrency])
|
||||
|
||||
useEffect(() => {
|
||||
const firstAvailablePayChannel = sortedPayChannels[0]
|
||||
|
||||
if (!firstAvailablePayChannel) {
|
||||
if (paymentChannelCode) {
|
||||
setPaymentChannelCode('')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const hasSelectedAvailablePayChannel = sortedPayChannels.some(
|
||||
(channel) => channel.code === paymentChannelCode,
|
||||
)
|
||||
|
||||
if (!hasSelectedAvailablePayChannel) {
|
||||
setPaymentChannelCode(firstAvailablePayChannel.code)
|
||||
}
|
||||
}, [paymentChannelCode, sortedPayChannels])
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedBanks.length === 0) {
|
||||
if (bankCode) {
|
||||
setBankCode('')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const hasSelectedAvailableBank = sortedBanks.some(
|
||||
(bank) => bank.code === bankCode,
|
||||
)
|
||||
|
||||
if (!hasSelectedAvailableBank) {
|
||||
setBankCode('')
|
||||
}
|
||||
}, [bankCode, sortedBanks])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitializedAmount && selectedRate > 0) {
|
||||
setAmount(getInitialWithdrawAmount(selectedRate, maxWithdrawAmount))
|
||||
setHasInitializedAmount(true)
|
||||
}
|
||||
}, [hasInitializedAmount, maxWithdrawAmount, selectedRate, setAmount])
|
||||
|
||||
useEffect(() => {
|
||||
if (amount > maxWithdrawAmount) {
|
||||
setAmount(maxWithdrawAmount)
|
||||
}
|
||||
}, [amount, maxWithdrawAmount, setAmount])
|
||||
|
||||
const quickAmounts = useMemo(() => {
|
||||
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({
|
||||
diamonds: Math.min(
|
||||
maxWithdrawAmount,
|
||||
Math.max(1, Math.round(selectedRate * fiatAmount)),
|
||||
),
|
||||
id: `quick-${selectedCurrency.code}-${fiatAmount}`,
|
||||
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`,
|
||||
}))
|
||||
}, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate])
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
const nextCurrencyCode = config.currencies[0]?.code ?? 'MYR'
|
||||
const nextCurrency = getActiveCurrencyCode(
|
||||
config.currencies,
|
||||
nextCurrencyCode,
|
||||
)
|
||||
const nextRate = nextCurrency.withdrawCoinsPerFiatValue || 1
|
||||
|
||||
setAmountState(getInitialWithdrawAmount(nextRate, maxWithdrawAmount))
|
||||
setHasInitializedAmount(true)
|
||||
setCurrencyCode(nextCurrencyCode)
|
||||
setPaymentChannelCode(sortedPayChannels[0]?.code ?? '')
|
||||
setBankCode('')
|
||||
setHolderName('')
|
||||
setBankAccount('')
|
||||
setReceiverEmail('')
|
||||
setReceiverPhone('')
|
||||
}, [config.currencies, maxWithdrawAmount, sortedPayChannels])
|
||||
|
||||
const selectedCurrencyPreview = useMemo(
|
||||
() => ({
|
||||
currencyCode: selectedCurrency.code,
|
||||
currencyLabel: selectedCurrency.label,
|
||||
exchangeRateLabel: t('gameDesktop.withdraw.preview.exchangeRate', {
|
||||
currency: selectedCurrency.code,
|
||||
}),
|
||||
exchangeRateValue: t('gameDesktop.withdraw.preview.exchangeRateValue', {
|
||||
coins: formatNumber(locale, selectedRate),
|
||||
currency: selectedCurrency.code,
|
||||
platformCoinLabel: config.platformCoinLabel,
|
||||
}),
|
||||
convertibleLabel: t('gameDesktop.withdraw.preview.convertible', {
|
||||
currency: selectedCurrency.code,
|
||||
}),
|
||||
convertibleValue: `${formatNumber(
|
||||
locale,
|
||||
selectedRate > 0 ? amount / selectedRate : 0,
|
||||
)} ${selectedCurrency.code}`,
|
||||
}),
|
||||
[
|
||||
amount,
|
||||
config.platformCoinLabel,
|
||||
locale,
|
||||
selectedCurrency.code,
|
||||
selectedCurrency.label,
|
||||
selectedRate,
|
||||
t,
|
||||
],
|
||||
)
|
||||
|
||||
return {
|
||||
amount,
|
||||
amountExceedsBalance: amount > maxWithdrawAmount,
|
||||
amountRequiredError: amount <= 0,
|
||||
availableBalance,
|
||||
bankAccount,
|
||||
bankAccountError: bankAccount.trim().length === 0,
|
||||
bankCode,
|
||||
bankCodeError: bankCode.trim().length === 0,
|
||||
config,
|
||||
currencyCode,
|
||||
holderName,
|
||||
holderNameError: holderName.trim().length === 0,
|
||||
isLoading: withdrawConfigQuery.isLoading,
|
||||
isRefetching: withdrawConfigQuery.isFetching,
|
||||
maxWithdrawAmount,
|
||||
paymentChannelCode,
|
||||
paymentChannelCodeError: paymentChannelCode.trim().length === 0,
|
||||
quickAmounts,
|
||||
receiverEmail,
|
||||
receiverEmailError: !isValidEmail(receiverEmail),
|
||||
receiverPhone,
|
||||
receiverPhoneError: !isValidPhone(receiverPhone),
|
||||
selectedCurrency,
|
||||
selectedCurrencyPreview,
|
||||
selectedPaymentChannel,
|
||||
selectedRate,
|
||||
resetForm,
|
||||
setAmount,
|
||||
setBankAccount,
|
||||
setBankCode,
|
||||
setCurrencyCode,
|
||||
setHolderName,
|
||||
setPaymentChannelCode,
|
||||
setReceiverEmail,
|
||||
setReceiverPhone,
|
||||
sortedBanks,
|
||||
sortedPayChannels,
|
||||
withdrawCopy: {
|
||||
bankLabel: t('gameDesktop.withdraw.bank'),
|
||||
eWalletLabel: t('gameDesktop.withdraw.eWallet'),
|
||||
feeNote: config.withdraw.feeNote,
|
||||
noticeLabel: t('gameDesktop.withdraw.notice'),
|
||||
processingLabel: t('gameDesktop.withdraw.processingTime'),
|
||||
processingValue: config.withdraw.processingNote,
|
||||
rateHint: config.withdraw.rateHint,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user