Port modular features into boursecu and fix desktop login UX.
Add support chat, piggy funds, PWA banner, notification/SSE sync, and messenger legacy guards while keeping boursecu theme; also balance login panels and unblock login navigation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/** نوتیف مرورگر بدون Firebase — فقط با کلیک کاربر (gesture) قابل درخواست است. */
|
||||
/** نوتیف مرورگر بدون Firebase — ترجیحاً با gesture کاربر؛ بعد از لاگین هم تلاش میشود. */
|
||||
|
||||
export type BrowserNotifState =
|
||||
| 'unsupported'
|
||||
@@ -77,11 +77,28 @@ export async function requestBrowserNotificationPermission(): Promise<BrowserNot
|
||||
}
|
||||
}
|
||||
|
||||
/** لوگوی برند از کانفیگ؛ fallback به آیکونهای PWA */
|
||||
function resolveNotifIcon() {
|
||||
try {
|
||||
return new URL('/favicon.ico', window.location.origin).href
|
||||
const avatar = String(
|
||||
(window as Window & { __APP_CONFIG__?: { user?: { avatar?: string } } }).__APP_CONFIG__?.user
|
||||
?.avatar ?? '',
|
||||
).trim()
|
||||
if (avatar) {
|
||||
return avatar
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL('/192.png', window.location.origin).href
|
||||
} catch {
|
||||
try {
|
||||
return new URL('/favicon.png', window.location.origin).href
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +121,6 @@ export async function showBrowserNotification(options: ShowBrowserNotificationOp
|
||||
const url = options.url || '/appNotifications'
|
||||
const icon = resolveNotifIcon()
|
||||
|
||||
// ۱) Service Worker — روی Chrome/Android پایدارتر است
|
||||
try {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
@@ -124,7 +140,6 @@ export async function showBrowserNotification(options: ShowBrowserNotificationOp
|
||||
console.warn('SW showNotification failed, falling back', error)
|
||||
}
|
||||
|
||||
// ۲) Notification constructor
|
||||
try {
|
||||
const n = new Notification(title, {
|
||||
body,
|
||||
|
||||
+1576
File diff suppressed because it is too large
Load Diff
+23
-1
@@ -122,6 +122,27 @@ function isIphone() {
|
||||
return /iPhone/.test(userAgent)
|
||||
}
|
||||
|
||||
function createUuid() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
||||
const bytes = new Uint8Array(16)
|
||||
crypto.getRandomValues(bytes)
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20),
|
||||
].join('-')
|
||||
}
|
||||
return `${Date.now()}-${Math.floor(Math.random() * 1e9)}`
|
||||
}
|
||||
|
||||
export {
|
||||
getToken,
|
||||
setToken,
|
||||
@@ -131,5 +152,6 @@ export {
|
||||
isIphone,
|
||||
removeFingerPrint,
|
||||
getIsActiveFingerprint,
|
||||
setFingerPrint
|
||||
setFingerPrint,
|
||||
createUuid
|
||||
}
|
||||
|
||||
@@ -384,6 +384,15 @@ function handleAppNotificationPayload(payload: unknown) {
|
||||
void applyAppNotificationFromStream(payload as Record<string, unknown>)
|
||||
}
|
||||
|
||||
function handleUnreadCountPayload(payload: unknown) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
touchStreamActivity()
|
||||
void applyUnreadCountFromStreamPayload(payload as Record<string, unknown>)
|
||||
}
|
||||
|
||||
async function applyAppNotificationFromStream(payload: Record<string, unknown>) {
|
||||
try {
|
||||
const { bumpUnreadFromStream } = await import('@/composables/useAppNotificationBadge')
|
||||
@@ -393,6 +402,15 @@ async function applyAppNotificationFromStream(payload: Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUnreadCountFromStreamPayload(payload: Record<string, unknown>) {
|
||||
try {
|
||||
const { applyUnreadCountFromStream } = await import('@/composables/useAppNotificationBadge')
|
||||
applyUnreadCountFromStream(payload)
|
||||
} catch (error) {
|
||||
console.warn('Unread-count SSE apply failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
function bindStreamEvents(source: EventSource) {
|
||||
source.addEventListener('last', (event) => {
|
||||
try {
|
||||
@@ -426,6 +444,14 @@ function bindStreamEvents(source: EventSource) {
|
||||
}
|
||||
})
|
||||
|
||||
source.addEventListener('unread-count', (event) => {
|
||||
try {
|
||||
handleUnreadCountPayload(JSON.parse(event.data))
|
||||
} catch (error) {
|
||||
console.warn('Unread-count SSE parse failed', error)
|
||||
}
|
||||
})
|
||||
|
||||
source.addEventListener('ping', () => {
|
||||
touchStreamActivity()
|
||||
})
|
||||
|
||||
+14
-3
@@ -28,7 +28,18 @@ export function formatTradeGramDisplay(grams?: number | null): number {
|
||||
}
|
||||
|
||||
function feeRate() {
|
||||
return Number(serviceFeePercentage.value)
|
||||
return normalizeServiceFeeRatio(Number(serviceFeePercentage.value))
|
||||
}
|
||||
|
||||
/** نسبت کارمزد (۰٫۰۱ = ۱٪). مقدار ≥۱ یعنی درصد انسانی اشتباه وارد شده. */
|
||||
export function normalizeServiceFeeRatio(value: number): number {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return 0
|
||||
}
|
||||
if (value >= 1) {
|
||||
return value / 100
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export async function loadServiceFeeFromSettings() {
|
||||
@@ -39,7 +50,7 @@ export async function loadServiceFeeFromSettings() {
|
||||
(s) => s?.key === 'fee_service_charge_percentage' && s?.is_active === true
|
||||
)
|
||||
if (feeSetting?.value !== undefined && feeSetting?.value !== null) {
|
||||
serviceFeePercentage.value = Number(feeSetting.value)
|
||||
serviceFeePercentage.value = normalizeServiceFeeRatio(Number(feeSetting.value))
|
||||
}
|
||||
|
||||
const feeModeSetting = settings.find(
|
||||
@@ -58,7 +69,7 @@ export async function loadServiceFeeFromSettings() {
|
||||
}
|
||||
|
||||
export function calcFeeInGrams(goldValue?: number | null) {
|
||||
return parseFloat(((goldValue ?? 0) * Number(serviceFeePercentage.value)).toFixed(4))
|
||||
return parseFloat(((goldValue ?? 0) * feeRate()).toFixed(4))
|
||||
}
|
||||
|
||||
/** گرم معادل برای دریافت دقیق مبلغ ریالی در فروش */
|
||||
|
||||
@@ -88,8 +88,11 @@ export function parseSupportMessengersJson(raw?: string | null): SupportMessenge
|
||||
export function resolveSupportMessengersFromSettings(
|
||||
settings: Array<{ key?: string; value?: unknown }>,
|
||||
): SupportMessengerEntry[] {
|
||||
const fromJson = parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers'))
|
||||
if (fromJson.length > 0) return fromJson
|
||||
// اگر کلید جدید در تنظیمات هست (حتی خالی/null)، فقط همان لیست معتبر است — legacy بله را برنگردان
|
||||
const hasMessengersKey = settings.some((row) => row?.key === 'support_messengers')
|
||||
if (hasMessengersKey) {
|
||||
return parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers'))
|
||||
}
|
||||
|
||||
const legacyMessenger = getSupportSettingValue(settings, 'support_messenger').toLowerCase()
|
||||
const legacyId = getSupportSettingValue(settings, 'support_messenger_id')
|
||||
@@ -108,9 +111,8 @@ export function resolveSupportMessengerEntries(
|
||||
): ResolvedSupportMessengerEntry[] {
|
||||
const resolved = settings ?? supportMessengerSettings.value
|
||||
const text = (messageText ?? '').trim()
|
||||
const source = resolved.messengers?.length
|
||||
? resolved.messengers
|
||||
: resolveLegacySingleMessenger(resolved)
|
||||
// فقط لیست resolveشده؛ قبل از load خالی است تا legacy دوباره دکمه نسازد
|
||||
const source = Array.isArray(resolved.messengers) ? resolved.messengers : []
|
||||
|
||||
return source
|
||||
.map((entry) => {
|
||||
@@ -126,14 +128,6 @@ export function resolveSupportMessengerEntries(
|
||||
.filter((entry): entry is ResolvedSupportMessengerEntry => entry !== null)
|
||||
}
|
||||
|
||||
function resolveLegacySingleMessenger(settings: SupportMessengerSettings): SupportMessengerEntry[] {
|
||||
const messenger = (settings.messenger ?? '').trim().toLowerCase()
|
||||
const id = (settings.messengerId ?? '').trim()
|
||||
if (!messenger || !id) return []
|
||||
if (!VALID_SUPPORT_MESSENGERS.has(messenger)) return []
|
||||
return [{ messenger, id }]
|
||||
}
|
||||
|
||||
/** @deprecated از depositGuideText استفاده کنید */
|
||||
export const DEFAULT_SUPPORT_MESSENGER_TEXT = DEFAULT_DEPOSIT_GUIDE_TEXT
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/** toast اعلان درونبرنامه — بالای هدر (1999) و ناوبری (5000)، زیر اکشنشیت */
|
||||
export const APP_Z_TOAST = 5_050
|
||||
|
||||
/**
|
||||
* z-index مدال/اکشنشیت/دیالوگ.
|
||||
* بالای ناوبری (5000)، هدر (1999) و دکمه پشتیبانی (5100) مینشیند تا این کرومها
|
||||
|
||||
Reference in New Issue
Block a user