feat: align piggy, support chat and numeric input with modular app

Persian keyboards send ٫ ، and / as decimal or thousand separators, so amount
and weight fields now normalize input before parsing. Support chat reopens an
existing thread instead of starting a new one, and axios surfaces the server
message on failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 22:17:17 +03:30
co-authored by Cursor
parent d2183c9a3c
commit 5d3ca82236
16 changed files with 694 additions and 344 deletions
+56
View File
@@ -13,6 +13,49 @@ export function persianToEnglishDigits(value: string | number | null | undefined
.replace(/٫/g, '.')
}
/**
* اعداد فارسی/عربی + جداکننده‌های اعشار رایج ماشین‌حساب فارسی.
* - `commaAsThousand` true (مبلغ): `,` / `،` حذف می‌شوند
* - `commaAsThousand` false (وزن/اعشار): `,` / `،` هم → `.`
*/
export function normalizeDecimalInput(
val: string | number | null | undefined,
options?: { commaAsThousand?: boolean }
): string {
if (val == null || val === '') return ''
const commaAsThousand = options?.commaAsThousand ?? true
let normalized = persianToEnglishDigits(String(val))
if (commaAsThousand) {
normalized = normalized.replace(/[,،]/g, '')
} else {
normalized = normalized.replace(/[,،]/g, '.')
}
normalized = normalized
.replace(/\//g, '.')
.replace(/[^\d.]/g, '')
const firstDot = normalized.indexOf('.')
if (firstDot !== -1) {
normalized =
normalized.slice(0, firstDot + 1) +
normalized.slice(firstDot + 1).replace(/\./g, '')
}
return normalized
}
export function parseDecimalInput(
val: string | number | null | undefined,
options?: { commaAsThousand?: boolean }
): number {
const normalized = normalizeDecimalInput(val, options)
if (!normalized) return NaN
return Number(normalized)
}
export function shouldNormalizeInputDigits(options: {
isPassword?: boolean
type?: string
@@ -27,3 +70,16 @@ export function shouldNormalizeInputDigits(options: {
const mode = String(options.inputmode ?? '').trim().toLowerCase()
return mode === 'numeric' || mode === 'decimal' || mode === 'number'
}
/** فیلدهای وزن/مبلغ/عدد اعشاری که باید `/` را ممیز بگیرند */
export function shouldNormalizeDecimalSeparators(options: {
isPassword?: boolean
type?: string
inputmode?: string
}): boolean {
if (options.isPassword) return false
const type = String(options.type ?? '').trim().toLowerCase()
if (type === 'price' || type === 'number') return true
const mode = String(options.inputmode ?? '').trim().toLowerCase()
return mode === 'numeric' || mode === 'decimal' || mode === 'number'
}
+7 -13
View File
@@ -87,8 +87,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')
@@ -107,9 +110,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) => {
@@ -125,14 +127,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
+21 -4
View File
@@ -71,29 +71,46 @@ _axios.interceptors.response.use(
}
const apiMessage = extractApiErrorMessage(response?.data)
const status = Number(response?.status ?? 0)
if (response?.status === 403) {
showApiError(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', config)
showApiError(
withHttpStatus(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', 403),
config,
)
return Promise.reject(error)
}
if (response?.status === 422) {
if (apiMessage) {
showApiError(apiMessage, config)
showApiError(withHttpStatus(apiMessage, 422), config)
} else {
showApiError(withHttpStatus('خطا در اعتبارسنجی', 422), config)
}
return Promise.reject(error)
}
if (response) {
showApiError(apiMessage || DEFAULT_LOAD_ERROR, config)
if (status >= 500) {
showApiError(withHttpStatus(SERVER_ERROR, status), config)
} else {
showApiError(withHttpStatus(apiMessage || DEFAULT_LOAD_ERROR, status), config)
}
return Promise.reject(error)
}
showApiError(DEFAULT_LOAD_ERROR, config)
// بدون status (نه 4xx نه 5xx): قطع شبکه، timeout، DNS، …
showApiError(NETWORK_ERROR, config)
return Promise.reject(error)
})
const DEFAULT_LOAD_ERROR = 'خطا در بارگذاری اطلاعات'
const SERVER_ERROR = 'خطا در برقراری ارتباط با سرور'
const NETWORK_ERROR = 'لطفا اینترنت خود را بررسی کنید و مجددا تلاش کنید'
function withHttpStatus(message: string, status: number): string {
return `${message} (${status})`
}
function extractApiErrorMessage(data: unknown): string {
if (!data || typeof data !== 'object') {