feat(game): 添加游戏历史记录实时同步功能
- 实现了游戏历史记录的实时推送和展示功能 - 添加了总奖池金额显示在历史记录界面中 - 新增 PeriodPayoutHistoryEntry 和 PeriodPayoutEventData 类型定义 - 在游戏状态管理中添加了待处理赔付历史记录状态 - 实现了 WebSocket 消息解析器来处理赔付数据 - 添加了移动端和桌面端历史记录组件的实时数据展示 - 集成了注册邀请码查询参数功能并优化了注册表单逻辑 - 重构了历史记录视图模型以支持实时数据流处理
This commit is contained in:
@@ -50,11 +50,8 @@ export const SMS_CODE_COOLDOWN_FALLBACK_SECONDS = 60
|
|||||||
/** @description 发送短信验证码的业务事件类型,当前固定为注册场景。 */
|
/** @description 发送短信验证码的业务事件类型,当前固定为注册场景。 */
|
||||||
export const SMS_SEND_EVENT_REGISTER = 'user_register'
|
export const SMS_SEND_EVENT_REGISTER = 'user_register'
|
||||||
|
|
||||||
/** @description 注册邀请码默认值,URL 未携带邀请码参数时兜底。 */
|
|
||||||
export const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16'
|
|
||||||
|
|
||||||
/** @description 注册邀请码在 URL 查询参数中的字段名。 */
|
/** @description 注册邀请码在 URL 查询参数中的字段名。 */
|
||||||
export const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
|
export const REGISTER_INVITE_CODE_QUERY_PARAM = 'invit_code'
|
||||||
|
|
||||||
/** @description 触发登录提示(弹窗/Toast)的最小去重间隔,单位为毫秒。 */
|
/** @description 触发登录提示(弹窗/Toast)的最小去重间隔,单位为毫秒。 */
|
||||||
export const LOGIN_PROMPT_DEDUP_MS = 1200
|
export const LOGIN_PROMPT_DEDUP_MS = 1200
|
||||||
|
|||||||
@@ -231,6 +231,14 @@ export function DesktopGameHistory() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-[1fr_1fr] gap-design-5">
|
<div className="grid grid-cols-[1fr_1fr] gap-design-5">
|
||||||
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(94,212,230,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(12,78,94,0.14))] px-design-7 py-design-4">
|
||||||
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
|
{t('gameDesktop.history.totalPoolAmount')}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-design-14 font-bold text-[#D5FBFF] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(95,233,255,0.18)]">
|
||||||
|
{item.betAmountLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-7 py-design-4">
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-7 py-design-4">
|
||||||
<span className="shrink-0 text-[#84A2A2]">
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
{t('gameDesktop.history.payout')}
|
{t('gameDesktop.history.payout')}
|
||||||
@@ -239,7 +247,7 @@ export function DesktopGameHistory() {
|
|||||||
{item.winAmountLabel}
|
{item.winAmountLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-7 py-design-4">
|
<div className="col-span-2 flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-7 py-design-4">
|
||||||
<span className={'shrink-0 text-[#84A2A2]'}>
|
<span className={'shrink-0 text-[#84A2A2]'}>
|
||||||
{t('gameDesktop.history.winningResult')}
|
{t('gameDesktop.history.winningResult')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function HistoryRewardNumber({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(95,233,255,0.16)] bg-[rgba(6,28,39,0.38)] p-design-1 align-middle ${className ?? ''}`}
|
className={`inline-flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)]bg-[rgba(6,28,39,0.38)] p-design-1 align-middle ${className ?? ''}`}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<RewardSpriteImage
|
<RewardSpriteImage
|
||||||
@@ -185,6 +185,15 @@ export function MobileGameHistory() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-design-4">
|
<div className="grid grid-cols-2 gap-design-4">
|
||||||
|
<div className="flex min-h-design-22 items-center justify-between gap-design-4 rounded-[calc(var(--design-unit)*5)] border border-[rgba(94,212,230,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(12,78,94,0.14))] px-design-6 py-design-4">
|
||||||
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
|
{t('gameDesktop.history.totalPoolAmount')}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-design-11 font-bold text-[#D5FBFF]">
|
||||||
|
{item.betAmountLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex min-h-design-22 items-center justify-between gap-design-4 rounded-[calc(var(--design-unit)*5)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-6 py-design-4">
|
<div className="flex min-h-design-22 items-center justify-between gap-design-4 rounded-[calc(var(--design-unit)*5)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-6 py-design-4">
|
||||||
<span className="shrink-0 text-[#84A2A2]">
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
{t('gameDesktop.history.payout')}
|
{t('gameDesktop.history.payout')}
|
||||||
@@ -194,7 +203,7 @@ export function MobileGameHistory() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-h-design-22 items-center justify-between gap-design-4 rounded-[calc(var(--design-unit)*5)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-6 py-design-4">
|
<div className="col-span-2 flex min-h-design-22 items-center justify-between gap-design-4 rounded-[calc(var(--design-unit)*5)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-6 py-design-4">
|
||||||
<span className="shrink-0 text-[#84A2A2]">
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
{t('gameDesktop.history.winningResult')}
|
{t('gameDesktop.history.winningResult')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getGameBetMyOrders } from '@/api'
|
import { getGameBetMyOrders } from '@/api'
|
||||||
import { GAME_HISTORY_PAGE_SIZE } from '@/constants'
|
import { GAME_HISTORY_PAGE_SIZE } from '@/constants'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameRoundStore } from '@/store/game'
|
import { useGameRoundStore } from '@/store/game'
|
||||||
import type { HistoryResultState } from '@/type'
|
import type { HistoryResultState, PeriodPayoutHistoryEntry } from '@/type'
|
||||||
|
|
||||||
|
interface GameHistoryDisplayItem {
|
||||||
|
betAmountLabel: string
|
||||||
|
createdAtLabel: string
|
||||||
|
id: string
|
||||||
|
resultState: HistoryResultState
|
||||||
|
numbersLabel: string
|
||||||
|
numbers: number[]
|
||||||
|
orderNo: string
|
||||||
|
periodNo: string
|
||||||
|
resultNumber: number | null
|
||||||
|
resultNumberLabel: string
|
||||||
|
winAmountLabel: string
|
||||||
|
}
|
||||||
|
|
||||||
function formatCreatedTime(timestamp: number, locale: string) {
|
function formatCreatedTime(timestamp: number, locale: string) {
|
||||||
const date = new Date(timestamp * 1000)
|
const date = new Date(timestamp * 1000)
|
||||||
@@ -32,15 +46,45 @@ function formatNumbers(numbers: number[]) {
|
|||||||
return numbers.map((number) => String(number).padStart(2, '0')).join(', ')
|
return numbers.map((number) => String(number).padStart(2, '0')).join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toPayoutHistoryDisplayItem(
|
||||||
|
entry: PeriodPayoutHistoryEntry,
|
||||||
|
locale: string,
|
||||||
|
): GameHistoryDisplayItem {
|
||||||
|
return {
|
||||||
|
betAmountLabel: entry.betAmount,
|
||||||
|
createdAtLabel: formatCreatedTime(entry.serverTime, locale),
|
||||||
|
id: entry.id,
|
||||||
|
resultState: entry.betNumbers.includes(entry.resultNumber)
|
||||||
|
? ('win' satisfies HistoryResultState)
|
||||||
|
: ('lost' satisfies HistoryResultState),
|
||||||
|
numbersLabel: formatNumbers(entry.betNumbers),
|
||||||
|
numbers: entry.betNumbers,
|
||||||
|
orderNo: entry.id,
|
||||||
|
periodNo: entry.periodNo,
|
||||||
|
resultNumber: entry.resultNumber,
|
||||||
|
resultNumberLabel: String(entry.resultNumber).padStart(2, '0'),
|
||||||
|
winAmountLabel: entry.winAmount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useGameHistoryVm() {
|
export function useGameHistoryVm() {
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
const locale = i18n.resolvedLanguage ?? 'en-US'
|
||||||
const accessToken = useAuthStore((state) => state.accessToken)
|
const accessToken = useAuthStore((state) => state.accessToken)
|
||||||
const authStatus = useAuthStore((state) => state.status)
|
const authStatus = useAuthStore((state) => state.status)
|
||||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||||
const revealRoundId = useGameRoundStore(
|
const revealRoundId = useGameRoundStore(
|
||||||
(state) => state.revealAnimation.roundId,
|
(state) => state.revealAnimation.roundId,
|
||||||
)
|
)
|
||||||
const lastRevealedRoundRef = useRef<string | null>(null)
|
const pendingPayoutHistoryEntryId = useGameRoundStore(
|
||||||
|
(state) => state.pendingPayoutHistoryEntry?.id ?? null,
|
||||||
|
)
|
||||||
|
const consumePendingPayoutHistoryEntry = useGameRoundStore(
|
||||||
|
(state) => state.consumePendingPayoutHistoryEntry,
|
||||||
|
)
|
||||||
|
const [realtimeEntries, setRealtimeEntries] = useState<
|
||||||
|
PeriodPayoutHistoryEntry[]
|
||||||
|
>([])
|
||||||
|
|
||||||
const query = useInfiniteQuery({
|
const query = useInfiniteQuery({
|
||||||
queryKey: ['game', 'bet-my-orders', accessToken],
|
queryKey: ['game', 'bet-my-orders', accessToken],
|
||||||
@@ -62,7 +106,7 @@ export function useGameHistoryVm() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const items = useMemo(
|
const apiItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(query.data?.pages ?? []).flatMap((page) =>
|
(query.data?.pages ?? []).flatMap((page) =>
|
||||||
(page.list ?? []).map((entry) => {
|
(page.list ?? []).map((entry) => {
|
||||||
@@ -71,11 +115,8 @@ export function useGameHistoryVm() {
|
|||||||
const resultNumber = shouldHideResult ? null : entry.result_number
|
const resultNumber = shouldHideResult ? null : entry.result_number
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amountLabel: entry.total_amount,
|
betAmountLabel: entry.bet_amount,
|
||||||
createdAtLabel: formatCreatedTime(
|
createdAtLabel: formatCreatedTime(entry.create_time, locale),
|
||||||
entry.create_time,
|
|
||||||
i18n.resolvedLanguage ?? 'en-US',
|
|
||||||
),
|
|
||||||
id: entry.order_no,
|
id: entry.order_no,
|
||||||
resultState:
|
resultState:
|
||||||
resultNumber === null
|
resultNumber === null
|
||||||
@@ -93,33 +134,52 @@ export function useGameHistoryVm() {
|
|||||||
? '--'
|
? '--'
|
||||||
: String(resultNumber).padStart(2, '0'),
|
: String(resultNumber).padStart(2, '0'),
|
||||||
winAmountLabel: entry.win_amount,
|
winAmountLabel: entry.win_amount,
|
||||||
}
|
} satisfies GameHistoryDisplayItem
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[i18n.resolvedLanguage, query.data?.pages, revealPhase, revealRoundId],
|
[locale, query.data?.pages, revealPhase, revealRoundId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() => [
|
||||||
|
...realtimeEntries.map((entry) =>
|
||||||
|
toPayoutHistoryDisplayItem(entry, locale),
|
||||||
|
),
|
||||||
|
...apiItems.filter(
|
||||||
|
(item) =>
|
||||||
|
!realtimeEntries.some((entry) => entry.periodNo === item.periodNo),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[apiItems, locale, realtimeEntries],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (revealPhase !== 'result' || !revealRoundId) {
|
if (
|
||||||
|
revealPhase !== 'result' ||
|
||||||
|
!revealRoundId ||
|
||||||
|
!pendingPayoutHistoryEntryId
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastRevealedRoundRef.current === revealRoundId) {
|
if (authStatus !== 'authenticated') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authStatus !== 'authenticated' || query.isFetching || query.isLoading) {
|
const entry = consumePendingPayoutHistoryEntry(revealRoundId)
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRevealedRoundRef.current = revealRoundId
|
setRealtimeEntries((entries) => [
|
||||||
|
entry,
|
||||||
void query.refetch()
|
...entries.filter((item) => item.periodNo !== entry.periodNo),
|
||||||
|
])
|
||||||
}, [
|
}, [
|
||||||
authStatus,
|
authStatus,
|
||||||
query.isFetching,
|
consumePendingPayoutHistoryEntry,
|
||||||
query.isLoading,
|
pendingPayoutHistoryEntryId,
|
||||||
query.refetch,
|
|
||||||
revealPhase,
|
revealPhase,
|
||||||
revealRoundId,
|
revealRoundId,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
extractBetWinData,
|
extractBetWinData,
|
||||||
extractJackpotHitData,
|
extractJackpotHitData,
|
||||||
extractPeriodEventData,
|
extractPeriodEventData,
|
||||||
|
extractPeriodPayoutData,
|
||||||
extractPeriodTick,
|
extractPeriodTick,
|
||||||
extractServerTime,
|
extractServerTime,
|
||||||
extractUserStreakMessageData,
|
extractUserStreakMessageData,
|
||||||
@@ -180,7 +181,8 @@ function applyPeriodPayoutMessage(
|
|||||||
) {
|
) {
|
||||||
applyPeriodMessage(message, serverTime)
|
applyPeriodMessage(message, serverTime)
|
||||||
|
|
||||||
const period = extractPeriodEventData(message)
|
const payout = extractPeriodPayoutData(message)
|
||||||
|
const period = payout ?? extractPeriodEventData(message)
|
||||||
|
|
||||||
if (period?.resultNumber !== null && period?.resultNumber !== undefined) {
|
if (period?.resultNumber !== null && period?.resultNumber !== undefined) {
|
||||||
const roundState = useGameRoundStore.getState()
|
const roundState = useGameRoundStore.getState()
|
||||||
@@ -195,6 +197,21 @@ function applyPeriodPayoutMessage(
|
|||||||
roundId: period.periodNo,
|
roundId: period.periodNo,
|
||||||
winningCellId: period.resultNumber,
|
winningCellId: period.resultNumber,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (payout && payout.betNumbers.length > 0) {
|
||||||
|
roundState.setPendingPayoutHistoryEntry({
|
||||||
|
betAmount: payout.betAmount ?? '0.00',
|
||||||
|
betFlowerNames: payout.betFlowerNames,
|
||||||
|
betNumbers: payout.betNumbers,
|
||||||
|
id: `period-payout:${payout.periodNo}`,
|
||||||
|
periodNo: payout.periodNo,
|
||||||
|
resultFlowerName: payout.resultFlowerName,
|
||||||
|
resultNumber: period.resultNumber,
|
||||||
|
serverTime:
|
||||||
|
payout.serverTime ?? serverTime ?? Math.floor(Date.now() / 1000),
|
||||||
|
winAmount: payout.winAmount ?? '0.00',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const roundId = period?.periodNo ?? useGameRoundStore.getState().round.id
|
const roundId = period?.periodNo ?? useGameRoundStore.getState().round.id
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { registerWithPassword } from '@/api'
|
import { registerWithPassword } from '@/api'
|
||||||
import {
|
|
||||||
DEFAULT_REGISTER_INVITE_CODE,
|
|
||||||
REGISTER_INVITE_CODE_QUERY_PARAM,
|
|
||||||
} from '@/constants'
|
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { registerFormSchema } from '@/schema/auth-schema'
|
import { registerFormSchema } from '@/schema/auth-schema'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import type { RegisterFormValues, UseRegisterFormOptions } from '@/type'
|
import type { RegisterFormValues, UseRegisterFormOptions } from '@/type'
|
||||||
import { toAuthSubmitErrorKey } from './auth-error-key'
|
import { toAuthSubmitErrorKey } from './auth-error-key'
|
||||||
|
import { getRegisterInviteCodeFromSearch } from './use-register-invite-code'
|
||||||
import { createZodResolver } from './zod-form-resolver'
|
import { createZodResolver } from './zod-form-resolver'
|
||||||
|
|
||||||
function getInitialRegisterInviteCode() {
|
function getInitialRegisterInviteCode() {
|
||||||
if (typeof window === 'undefined') {
|
return getRegisterInviteCodeFromSearch()
|
||||||
return DEFAULT_REGISTER_INVITE_CODE
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
new URLSearchParams(window.location.search)
|
|
||||||
.get(REGISTER_INVITE_CODE_QUERY_PARAM)
|
|
||||||
?.trim() || DEFAULT_REGISTER_INVITE_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
|
export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
|
||||||
|
|||||||
41
src/hooks/use-register-invite-code.ts
Normal file
41
src/hooks/use-register-invite-code.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { REGISTER_INVITE_CODE_QUERY_PARAM } from '@/constants'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import { useModalStore } from '@/store/modal'
|
||||||
|
|
||||||
|
export function getRegisterInviteCodeFromSearch(search?: string) {
|
||||||
|
const sourceSearch =
|
||||||
|
search ?? (typeof window === 'undefined' ? '' : window.location.search)
|
||||||
|
|
||||||
|
return (
|
||||||
|
new URLSearchParams(sourceSearch)
|
||||||
|
.get(REGISTER_INVITE_CODE_QUERY_PARAM)
|
||||||
|
?.trim() ?? ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoOpenRegisterByInviteCode() {
|
||||||
|
const handledInviteCodeRef = useRef<string | null>(null)
|
||||||
|
const authIsHydrated = useAuthStore((state) => state.isHydrated)
|
||||||
|
const authStatus = useAuthStore((state) => state.status)
|
||||||
|
const openExclusiveModal = useModalStore((state) => state.openExclusiveModal)
|
||||||
|
const inviteCode = useMemo(() => getRegisterInviteCodeFromSearch(), [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!authIsHydrated ||
|
||||||
|
!inviteCode ||
|
||||||
|
handledInviteCodeRef.current === inviteCode
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handledInviteCodeRef.current = inviteCode
|
||||||
|
|
||||||
|
if (authStatus === 'authenticated') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openExclusiveModal('desktopRegister')
|
||||||
|
}, [authIsHydrated, authStatus, inviteCode, openExclusiveModal])
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
JackpotHitEventDataDto,
|
JackpotHitEventDataDto,
|
||||||
JackpotHitItemDto,
|
JackpotHitItemDto,
|
||||||
PeriodEventData,
|
PeriodEventData,
|
||||||
|
PeriodPayoutEventData,
|
||||||
UserStreakMessageData,
|
UserStreakMessageData,
|
||||||
WalletChangedData,
|
WalletChangedData,
|
||||||
} from '@/type'
|
} from '@/type'
|
||||||
@@ -187,6 +188,58 @@ export function extractPeriodEventData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toNumberArray(value: unknown) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.map((item) => toOptionalNumber(item))
|
||||||
|
.filter(
|
||||||
|
(item): item is number =>
|
||||||
|
typeof item === 'number' && Number.isInteger(item),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStringArray(value: unknown) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.map((item) => toOptionalString(item))
|
||||||
|
.filter((item): item is string => typeof item === 'string')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPeriodPayoutData(
|
||||||
|
message: GameSocketMessage,
|
||||||
|
): PeriodPayoutEventData | null {
|
||||||
|
const period = extractPeriodEventData(message)
|
||||||
|
|
||||||
|
if (!period) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getNestedRecord(message, 'data')
|
||||||
|
const source = data ?? (message as Record<string, unknown>)
|
||||||
|
const root = message as Record<string, unknown>
|
||||||
|
|
||||||
|
return {
|
||||||
|
...period,
|
||||||
|
betAmount: toOptionalString(source.bet_amount) ?? null,
|
||||||
|
betFlowerNames: toStringArray(source.bet_flower_names),
|
||||||
|
betNumbers: toNumberArray(source.bet_numbers),
|
||||||
|
payoutRemainingSeconds:
|
||||||
|
toOptionalNumber(source.payout_remaining_seconds) ?? null,
|
||||||
|
payoutSeconds: toOptionalNumber(source.payout_seconds) ?? null,
|
||||||
|
payoutUntil: toOptionalNumber(source.payout_until) ?? null,
|
||||||
|
resultFlowerName: toOptionalString(source.result_flower_name) ?? null,
|
||||||
|
serverTime:
|
||||||
|
toOptionalNumber(source.server_time ?? root.server_time) ?? null,
|
||||||
|
winAmount: toOptionalString(source.win_amount) ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function extractWalletChangedData(
|
export function extractWalletChangedData(
|
||||||
message: GameSocketMessage,
|
message: GameSocketMessage,
|
||||||
): WalletChangedData | null {
|
): WalletChangedData | null {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { getGameLobbyInit } from '@/api'
|
import { getGameLobbyInit } from '@/api'
|
||||||
import { MOBILE_LAYOUT_BREAKPOINT_PX } from '@/constants'
|
import { MOBILE_LAYOUT_BREAKPOINT_PX } from '@/constants'
|
||||||
import { useGameRealtimeSync } from '@/hooks/use-game-realtime-sync.ts'
|
import { useGameRealtimeSync } from '@/hooks/use-game-realtime-sync.ts'
|
||||||
|
import { useAutoOpenRegisterByInviteCode } from '@/hooks/use-register-invite-code'
|
||||||
import { useDocumentMetadata } from '@/lib/head/document-metadata.ts'
|
import { useDocumentMetadata } from '@/lib/head/document-metadata.ts'
|
||||||
import { notify } from '@/lib/notify.ts'
|
import { notify } from '@/lib/notify.ts'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
@@ -24,6 +25,7 @@ const PcEntry = lazy(async () => {
|
|||||||
export function MainEntryPage() {
|
export function MainEntryPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
useGameRealtimeSync()
|
useGameRealtimeSync()
|
||||||
|
useAutoOpenRegisterByInviteCode()
|
||||||
const hydrateRound = useGameRoundStore((state) => state.hydrateRound)
|
const hydrateRound = useGameRoundStore((state) => state.hydrateRound)
|
||||||
const selectChip = useGameRoundStore((state) => state.selectChip)
|
const selectChip = useGameRoundStore((state) => state.selectChip)
|
||||||
const hydrateSession = useGameSessionStore((state) => state.hydrateSession)
|
const hydrateSession = useGameSessionStore((state) => state.hydrateSession)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
GameRoundSlice,
|
GameRoundSlice,
|
||||||
GameRoundStoreState,
|
GameRoundStoreState,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
|
PeriodPayoutHistoryEntry,
|
||||||
RevealAnimationState,
|
RevealAnimationState,
|
||||||
RoundSnapshot,
|
RoundSnapshot,
|
||||||
TrendEntry,
|
TrendEntry,
|
||||||
@@ -119,6 +120,7 @@ function resolveSelectionQuantity(
|
|||||||
function createInitialRoundState(): GameRoundSlice & {
|
function createInitialRoundState(): GameRoundSlice & {
|
||||||
activeChipId: string
|
activeChipId: string
|
||||||
activeBetQuantity: number
|
activeBetQuantity: number
|
||||||
|
pendingPayoutHistoryEntry: PeriodPayoutHistoryEntry | null
|
||||||
recentSuccessfulSelections: BetSelection[]
|
recentSuccessfulSelections: BetSelection[]
|
||||||
revealAnimation: RevealAnimationState
|
revealAnimation: RevealAnimationState
|
||||||
} {
|
} {
|
||||||
@@ -131,6 +133,7 @@ function createInitialRoundState(): GameRoundSlice & {
|
|||||||
chips: snapshot.chips,
|
chips: snapshot.chips,
|
||||||
history: snapshot.history,
|
history: snapshot.history,
|
||||||
maxSelectionCount: snapshot.maxSelectionCount,
|
maxSelectionCount: snapshot.maxSelectionCount,
|
||||||
|
pendingPayoutHistoryEntry: null,
|
||||||
recentSuccessfulSelections: [],
|
recentSuccessfulSelections: [],
|
||||||
revealAnimation: createIdleRevealAnimation(),
|
revealAnimation: createIdleRevealAnimation(),
|
||||||
round: snapshot.round,
|
round: snapshot.round,
|
||||||
@@ -139,7 +142,7 @@ function createInitialRoundState(): GameRoundSlice & {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
export const useGameRoundStore = create<GameRoundStoreState>()((set, get) => ({
|
||||||
...createInitialRoundState(),
|
...createInitialRoundState(),
|
||||||
adjustBetQuantity: (delta) => {
|
adjustBetQuantity: (delta) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -181,6 +184,18 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
consumePendingPayoutHistoryEntry: (roundId) => {
|
||||||
|
const state = get()
|
||||||
|
const entry = state.pendingPayoutHistoryEntry
|
||||||
|
|
||||||
|
if (!entry || entry.periodNo !== roundId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ pendingPayoutHistoryEntry: null })
|
||||||
|
|
||||||
|
return entry
|
||||||
|
},
|
||||||
finishRevealAnimation: () => {
|
finishRevealAnimation: () => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (
|
if (
|
||||||
@@ -244,6 +259,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
pendingPayoutHistoryEntry: null,
|
||||||
placeBet: (cellId) => {
|
placeBet: (cellId) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const activeChip =
|
const activeChip =
|
||||||
@@ -328,7 +344,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
restoreRecentSuccessfulSelections: () => {
|
restoreRecentSuccessfulSelections: () => {
|
||||||
const state = useGameRoundStore.getState()
|
const state = get()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
state.round.phase !== 'betting' ||
|
state.round.phase !== 'betting' ||
|
||||||
@@ -468,6 +484,9 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
setPendingPayoutHistoryEntry: (entry) => {
|
||||||
|
set({ pendingPayoutHistoryEntry: entry })
|
||||||
|
},
|
||||||
syncRound: (round) => {
|
syncRound: (round) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const previousRound = state.round
|
const previousRound = state.round
|
||||||
|
|||||||
@@ -212,6 +212,30 @@ export interface PeriodEventData {
|
|||||||
resultNumber: number | null
|
resultNumber: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PeriodPayoutHistoryEntry {
|
||||||
|
betAmount: string
|
||||||
|
betFlowerNames: string[]
|
||||||
|
betNumbers: number[]
|
||||||
|
id: string
|
||||||
|
periodNo: string
|
||||||
|
resultFlowerName: string | null
|
||||||
|
resultNumber: number
|
||||||
|
serverTime: number
|
||||||
|
winAmount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeriodPayoutEventData extends PeriodEventData {
|
||||||
|
betAmount: string | null
|
||||||
|
betFlowerNames: string[]
|
||||||
|
betNumbers: number[]
|
||||||
|
payoutRemainingSeconds: number | null
|
||||||
|
payoutSeconds: number | null
|
||||||
|
payoutUntil: number | null
|
||||||
|
resultFlowerName: string | null
|
||||||
|
serverTime: number | null
|
||||||
|
winAmount: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface WalletChangedData {
|
export interface WalletChangedData {
|
||||||
coin: string
|
coin: string
|
||||||
}
|
}
|
||||||
@@ -550,6 +574,7 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
|||||||
hydrateRound: (snapshot: GameRoundSlice) => void
|
hydrateRound: (snapshot: GameRoundSlice) => void
|
||||||
placeBet: (cellId: number) => void
|
placeBet: (cellId: number) => void
|
||||||
playPreparedRevealAnimation: (roundId?: string | null) => void
|
playPreparedRevealAnimation: (roundId?: string | null) => void
|
||||||
|
pendingPayoutHistoryEntry: PeriodPayoutHistoryEntry | null
|
||||||
prepareRevealAnimation: (input: {
|
prepareRevealAnimation: (input: {
|
||||||
revealKey: string
|
revealKey: string
|
||||||
roundId: string
|
roundId: string
|
||||||
@@ -559,6 +584,9 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
|||||||
revealAnimation: RevealAnimationState
|
revealAnimation: RevealAnimationState
|
||||||
removeSelectionsForCell: (cellId: number) => void
|
removeSelectionsForCell: (cellId: number) => void
|
||||||
restoreRecentSuccessfulSelections: () => boolean
|
restoreRecentSuccessfulSelections: () => boolean
|
||||||
|
consumePendingPayoutHistoryEntry: (
|
||||||
|
roundId: string,
|
||||||
|
) => PeriodPayoutHistoryEntry | null
|
||||||
setRecentSuccessfulSelections: (selections: BetSelection[]) => void
|
setRecentSuccessfulSelections: (selections: BetSelection[]) => void
|
||||||
selectChip: (chipId: string) => void
|
selectChip: (chipId: string) => void
|
||||||
setPhase: (phase: RoundPhase) => void
|
setPhase: (phase: RoundPhase) => void
|
||||||
@@ -569,6 +597,7 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
|||||||
totalWin: string
|
totalWin: string
|
||||||
winningCellId?: number | null
|
winningCellId?: number | null
|
||||||
}) => void
|
}) => void
|
||||||
|
setPendingPayoutHistoryEntry: (entry: PeriodPayoutHistoryEntry | null) => void
|
||||||
syncRound: (round: Partial<RoundSnapshot>) => void
|
syncRound: (round: Partial<RoundSnapshot>) => void
|
||||||
upsertSelections: (selections: BetSelection[]) => void
|
upsertSelections: (selections: BetSelection[]) => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user