diff --git a/src/app/bootstrap.ts b/src/app/bootstrap.ts index 3a9c1ea..00fdd30 100644 --- a/src/app/bootstrap.ts +++ b/src/app/bootstrap.ts @@ -113,7 +113,9 @@ export async function bootstrapApp() { await withTimeout(settingsStore.fetchSettings(), 45000, 'Settings loading timeout') - initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key)) + if (getToken()) { + initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key)) + } if (getToken()) { await withTimeout(userStore.getMyInfo(), 45000, 'User session loading timeout') diff --git a/src/composables/useWalletAssets.ts b/src/composables/useWalletAssets.ts index 8cb9bf4..717f171 100644 --- a/src/composables/useWalletAssets.ts +++ b/src/composables/useWalletAssets.ts @@ -1,12 +1,57 @@ import { ref } from 'vue' import { getWalletAssets } from '@/api/wallet' -import { getProductsCatalog } from '@/composables/useProductsCatalog' +import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog' +import { useWalletStore } from '@/store/walletStore' import type { WalletAssetsSummary } from '@/models/walletAssets' import { enrichInventoryCardsWithCatalog } from '@/utils/productAssets' const assetsSummary = ref(null) let assetsPromise: Promise | null = null +function enrichAssetsSummary(data: WalletAssetsSummary): WalletAssetsSummary { + const catalog = getProductsCatalogRef().value + + return { + ...data, + inventory_cards: enrichInventoryCardsWithCatalog(data.inventory_cards ?? [], catalog ?? []), + count_products: enrichInventoryCardsWithCatalog(data.count_products ?? [], catalog ?? []), + } +} + +function syncWalletStoreFromAssets(data: WalletAssetsSummary) { + const walletStore = useWalletStore() + if (!walletStore.walletInfo) { + return + } + + const goldMetal = data.metal_totals?.find((metal) => metal.metal_id === 1) ?? data.metal_totals?.[0] + + walletStore.walletInfo.cash = data.cash_wallet ?? walletStore.walletInfo.cash + + if (goldMetal) { + walletStore.walletInfo.gold = goldMetal.melted_weight ?? walletStore.walletInfo.gold + walletStore.walletInfo.gold_sum = goldMetal.total_weight ?? walletStore.walletInfo.gold_sum + } +} + +export async function applyWalletAssetsFromStream(payload: unknown) { + if (!payload || typeof payload !== 'object') { + return + } + + const data = payload as WalletAssetsSummary + if (data.meta == null) { + return + } + + if (!getProductsCatalogRef().value.length) { + await getProductsCatalog() + } + + assetsSummary.value = enrichAssetsSummary(data) + syncWalletStoreFromAssets(assetsSummary.value) +} + export function useWalletAssets() { async function loadWalletAssets(force = false) { if (!force && assetsSummary.value) { @@ -17,14 +62,10 @@ export function useWalletAssets() { return assetsPromise } - // Reuses cached getProductsCatalog() — same catalog as chart offset logic assetsPromise = Promise.all([getWalletAssets(), getProductsCatalog(force)]) - .then(([data, catalog]) => { - assetsSummary.value = { - ...data, - inventory_cards: enrichInventoryCardsWithCatalog(data.inventory_cards ?? [], catalog ?? []), - count_products: enrichInventoryCardsWithCatalog(data.count_products ?? [], catalog ?? []), - } + .then(([data]) => { + assetsSummary.value = enrichAssetsSummary(data) + syncWalletStoreFromAssets(assetsSummary.value) return assetsSummary.value }) .finally(() => { diff --git a/src/utils/index.ts b/src/utils/index.ts index 407be0b..1e24f3b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,7 @@ import { findStoredAuthToken, persistAuthToken, } from '@/utils/authToken' +import { stopPriceRealtimeAuth } from '@/utils/priceSocket' import { ref } from 'vue' import {useConfigStore} from "@/store/configStore"; @@ -85,6 +86,7 @@ export async function authenticateWithFingerprint() { function removeToken() { + stopPriceRealtimeAuth() return clearStoredAuthTokens() } diff --git a/src/utils/priceSocket.ts b/src/utils/priceSocket.ts index e62282c..cf7d0e9 100644 --- a/src/utils/priceSocket.ts +++ b/src/utils/priceSocket.ts @@ -2,6 +2,7 @@ import type { Price } from '@/models/home' import type { Product } from '@/models/products' import { Modal } from 'ant-design-vue' import { getConfigApp } from '@/utils/config' +import { findStoredAuthToken } from '@/utils/authToken' import { priceSocketConnected, priceStreamLifecycle, priceWssEnabled } from '@/utils/priceSocketState' export { priceSocketConnected, priceStreamLifecycle, priceWssEnabled, shouldUsePricePolling } from '@/utils/priceSocketState' @@ -56,8 +57,16 @@ function resolveStreamUrl(): string | null { return null } + const token = findStoredAuthToken() + if (!token) { + return null + } + const base = String(getConfigApp('node_server') || window.location.origin).replace(/\/$/, '') - return `${base}${resolveStreamPath()}` + const url = new URL(`${base}${resolveStreamPath()}`) + url.searchParams.set('token', token) + + return url.toString() } function clearReconnectTimer() { @@ -205,6 +214,11 @@ async function applyListFromStream(products: Product[]) { applyProductsListFromSocket(products) } +async function applyAssetsFromStream(payload: unknown) { + const { applyWalletAssetsFromStream } = await import('@/composables/useWalletAssets') + applyWalletAssetsFromStream(payload) +} + function handleListPayload(payload: unknown) { if (!Array.isArray(payload) || !payload.length) { return @@ -214,6 +228,15 @@ function handleListPayload(payload: unknown) { void applyListFromStream(payload as Product[]) } +function handleAssetsPayload(payload: unknown) { + if (!payload || typeof payload !== 'object') { + return + } + + touchStreamActivity() + void applyAssetsFromStream(payload) +} + function bindStreamEvents(source: EventSource) { source.addEventListener('last', (event) => { try { @@ -231,6 +254,14 @@ function bindStreamEvents(source: EventSource) { } }) + source.addEventListener('assets', (event) => { + try { + handleAssetsPayload(JSON.parse(event.data)) + } catch (error) { + console.warn('Price SSE assets parse failed', error) + } + }) + source.addEventListener('ping', () => { touchStreamActivity() }) @@ -414,27 +445,58 @@ function onVisibilityChange() { export function isPriceWebSocketEnabled(settingGetter: (key: string) => unknown): boolean { getSetting = settingGetter + if (!isTruthySetting(settingGetter('wss_enabled'), true)) { + return false + } + + if (!findStoredAuthToken()) { + return false + } + return resolveStreamUrl() !== null } let channelStarted = false -export function initPriceRealtimeChannel(settingGetter: (key: string) => unknown) { +function shutdownPriceRealtimeChannel() { + if (!channelStarted && subscribers <= 0) { + return + } + + channelStarted = false + stopped = true + subscribers = 0 + disconnect() + unbindBrowserNetworkListeners() + + if (typeof document !== 'undefined') { + document.removeEventListener('visibilitychange', onVisibilityChange) + } +} + +export function syncPriceRealtimeChannel(settingGetter: (key: string) => unknown) { + getSetting = settingGetter priceWssEnabled.value = isTruthySetting(settingGetter('wss_enabled'), true) - if (channelStarted) { + if (!priceWssEnabled.value || !isPriceWebSocketEnabled(settingGetter)) { + shutdownPriceRealtimeChannel() + return + } + + if (channelStarted && subscribers > 0) { + forceReconnect() return } channelStarted = true - - if (!priceWssEnabled.value) { - return - } - - if (!isPriceWebSocketEnabled(settingGetter)) { - return - } - + stopped = false startPriceSocket(settingGetter) } + +export function initPriceRealtimeChannel(settingGetter: (key: string) => unknown) { + syncPriceRealtimeChannel(settingGetter) +} + +export function stopPriceRealtimeAuth() { + shutdownPriceRealtimeChannel() +} diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue index 0a37a32..c6897f4 100644 --- a/src/views/login/components/Login.vue +++ b/src/views/login/components/Login.vue @@ -26,7 +26,7 @@ 'z-100 self-center w-36', config.config?.name === 'garoosi' ? 'h-auto object-contain' - : 'rounded-full aspect-square', + : 'rounded aspect-square', ]" style="margin-bottom: 60px" > @@ -375,6 +375,7 @@ import router from '@/router' import { useUserStore } from '@/store/userStore' import { useWalletStore } from '@/store/walletStore' import { useSettingsStore } from '@/store/settingsStore' +import { syncPriceRealtimeChannel } from '@/utils/priceSocket' import { setTruth } from '@/app/authBoot' import {useConfigStore} from "@/store/configStore"; import { @@ -480,6 +481,7 @@ async function handleOnComplete(value: string) { async function login(accessToken: string | undefined) { setToken(accessToken) + syncPriceRealtimeChannel((key) => setting.getSettingByKey(key)) await userStore.getMyInfo() // console.log(URLUserType()) // console.log(userStore.userInfo?.type) diff --git a/src/views/login/components/Register.vue b/src/views/login/components/Register.vue index 010c0b3..2ea604a 100644 --- a/src/views/login/components/Register.vue +++ b/src/views/login/components/Register.vue @@ -26,7 +26,7 @@ 'z-100 self-center w-36', config.config?.name === 'garoosi' ? 'h-auto object-contain mb-[60px]' - : 'rounded-full aspect-square', + : 'rounded aspect-square', ]" >
ثبت نام