Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.0 KiB
TypeScript
190 lines
5.0 KiB
TypeScript
import {
|
|
allowSuperAdminFallback,
|
|
getSuperAdminBases,
|
|
isSuperAdminUrl,
|
|
resolveTenantDomain,
|
|
SA_RETRY_ROUNDS,
|
|
} from '@/config/superadmin'
|
|
|
|
type VersionResponse =
|
|
| string
|
|
| number
|
|
| {
|
|
version?: string | number
|
|
data?: string | number | { version?: string | number; value?: string | number }
|
|
value?: string | number
|
|
user?: unknown
|
|
}
|
|
|
|
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
|
|
export const VERSION_RETRY_ROUNDS = SA_RETRY_ROUNDS
|
|
|
|
let versionFetchPromise: Promise<string | null> | null = null
|
|
|
|
/** فقط SuperAdmin — بدون رجیستری قدیمی */
|
|
function getVersionUrls() {
|
|
const domain = encodeURIComponent(resolveTenantDomain())
|
|
return getSuperAdminBases().map((base) => `${base}/get_version?domain=${domain}`)
|
|
}
|
|
|
|
function sortVersionUrls(urls: string[]) {
|
|
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
|
if (!lastSuccessUrl || !urls.includes(lastSuccessUrl) || !isSuperAdminUrl(lastSuccessUrl)) {
|
|
if (lastSuccessUrl && !isSuperAdminUrl(lastSuccessUrl)) {
|
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
}
|
|
return [...urls]
|
|
}
|
|
return [lastSuccessUrl, ...urls.filter((url) => url !== lastSuccessUrl)]
|
|
}
|
|
|
|
function clearStaleVersionSuccessUrl(url: string) {
|
|
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
|
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
}
|
|
}
|
|
|
|
function readUserVersion(user: unknown) {
|
|
if (!user || typeof user !== 'object' || Array.isArray(user)) {
|
|
return ''
|
|
}
|
|
|
|
const record = user as { version?: string | number; value?: string | number }
|
|
const version = record.version ?? record.value
|
|
|
|
return version === undefined || version === null ? '' : String(version).trim()
|
|
}
|
|
|
|
function normalizeVersionResponse(response: VersionResponse) {
|
|
if (typeof response === 'string' || typeof response === 'number') {
|
|
return String(response).trim()
|
|
}
|
|
|
|
if (!response) return ''
|
|
|
|
if (typeof response.data === 'string' || typeof response.data === 'number') {
|
|
return String(response.data).trim()
|
|
}
|
|
|
|
const directVersion =
|
|
response.version ??
|
|
response.value ??
|
|
(typeof response.data === 'object' ? response.data?.version ?? response.data?.value : undefined)
|
|
|
|
if (directVersion !== undefined && directVersion !== null) {
|
|
return String(directVersion).trim()
|
|
}
|
|
|
|
return readUserVersion(response.user)
|
|
}
|
|
|
|
async function fetchVersion(url: string, timeout = 3500): Promise<string | null> {
|
|
const controller = new AbortController()
|
|
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
cache: 'no-store',
|
|
signal: controller.signal,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Version request failed with status ${response.status}`)
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') ?? ''
|
|
if (!contentType.includes('application/json')) {
|
|
throw new Error('Version response is not JSON')
|
|
}
|
|
|
|
const data = (await response.json()) as VersionResponse
|
|
|
|
const version = normalizeVersionResponse(data)
|
|
|
|
if (!version) {
|
|
throw new Error('Version response is empty')
|
|
}
|
|
|
|
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
|
return version
|
|
} finally {
|
|
window.clearTimeout(timeoutId)
|
|
}
|
|
}
|
|
|
|
async function tryFetchVersionFromUrls(
|
|
urls: string[],
|
|
rounds: number,
|
|
): Promise<string | null> {
|
|
if (!urls.length) return null
|
|
|
|
for (let round = 1; round <= rounds; round++) {
|
|
for (let index = 0; index < urls.length; index++) {
|
|
const url = urls[index]
|
|
const timeout = index === urls.length - 1 ? 6000 : 3500
|
|
|
|
try {
|
|
const version = await fetchVersion(url, timeout)
|
|
if (version) {
|
|
return version
|
|
}
|
|
} catch (error) {
|
|
clearStaleVersionSuccessUrl(url)
|
|
console.warn(`Version failed from ${url}. Round: ${round}`, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
async function fetchVersionWithRetry(rounds = VERSION_RETRY_ROUNDS): Promise<string | null> {
|
|
const urls = sortVersionUrls(getVersionUrls())
|
|
const saUrls = urls.filter(isSuperAdminUrl)
|
|
const otherUrls = urls.filter((url) => !isSuperAdminUrl(url))
|
|
|
|
const lists =
|
|
saUrls.length > 0
|
|
? allowSuperAdminFallback()
|
|
? [saUrls, otherUrls]
|
|
: [saUrls]
|
|
: [urls]
|
|
|
|
for (const list of lists) {
|
|
const version = await tryFetchVersionFromUrls(list, rounds)
|
|
if (version) return version
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function resetVersionCheckCache() {
|
|
versionFetchPromise = null
|
|
}
|
|
|
|
export async function loadRemoteAppVersion(options?: {
|
|
rounds?: number
|
|
force?: boolean
|
|
}): Promise<string | null> {
|
|
const rounds = options?.rounds ?? VERSION_RETRY_ROUNDS
|
|
const force = options?.force ?? false
|
|
|
|
if (force) {
|
|
resetVersionCheckCache()
|
|
}
|
|
|
|
if (versionFetchPromise) {
|
|
return versionFetchPromise
|
|
}
|
|
|
|
versionFetchPromise = fetchVersionWithRetry(rounds).finally(() => {
|
|
versionFetchPromise = null
|
|
})
|
|
|
|
return versionFetchPromise
|
|
}
|