Merge remote-tracking branch 'origin/boursecu' into boursecu

This commit is contained in:
2026-07-23 12:27:51 +03:30
7 changed files with 452 additions and 273 deletions
+295
View File
@@ -0,0 +1,295 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { persianToEnglishDigits } from '@/utils/persianDigits'
import {
beginSmsOtpCapture,
clearPendingSmsOtp,
subscribeSmsOtp,
} from '@/utils/webOtp'
const props = withDefaults(
defineProps<{
modelValue?: string
length?: number
autofocus?: boolean
}>(),
{
modelValue: '',
length: 4,
autofocus: true,
},
)
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [value: string]
complete: [value: string]
}>()
const inputRef = ref<HTMLInputElement | null>(null)
const localValue = ref('')
let completedFor = ''
let pollTimer: ReturnType<typeof setInterval> | null = null
let unsubscribeOtp: (() => void) | null = null
const digits = computed(() => {
const chars = localValue.value.padEnd(props.length, ' ').slice(0, props.length).split('')
return chars.map((ch) => (ch === ' ' ? '' : ch))
})
function normalize(raw: string) {
return persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, props.length)
}
function applyCode(raw: string) {
const code = normalize(raw)
if (!code || code === localValue.value) {
return
}
localValue.value = code
if (inputRef.value && inputRef.value.value !== code) {
inputRef.value.value = code
}
emit('update:modelValue', code)
emit('change', code)
if (code.length === props.length && completedFor !== code) {
completedFor = code
clearPendingSmsOtp()
emit('complete', code)
}
if (code.length < props.length) {
completedFor = ''
}
}
function syncFromDom() {
const direct = inputRef.value?.value ?? ''
if (direct) {
applyCode(direct)
return
}
// اگر Chrome جای دیگری با autocomplete پر کرده
const nodes = document.querySelectorAll<HTMLInputElement>(
'input[autocomplete="one-time-code"]',
)
for (const node of nodes) {
const value = node.value ?? ''
if (normalize(value).length === props.length) {
applyCode(value)
return
}
}
}
function onInput(event: Event) {
applyCode((event.target as HTMLInputElement).value)
}
function onChange(event: Event) {
applyCode((event.target as HTMLInputElement).value)
}
function onAnimationStart(event: AnimationEvent) {
if (!String(event.animationName || '').includes('onAutoFillStart')) {
return
}
syncFromDom()
}
function focusInput() {
const el = inputRef.value
if (!el) return
el.focus({ preventScroll: true })
try {
el.setSelectionRange(el.value.length, el.value.length)
} catch {
/* ignore */
}
}
function clearInput() {
completedFor = ''
localValue.value = ''
clearPendingSmsOtp()
if (inputRef.value) {
inputRef.value.value = ''
}
emit('update:modelValue', '')
emit('change', '')
focusInput()
}
function fillInput(value: string) {
applyCode(value)
}
function startWebOtp() {
beginSmsOtpCapture(props.length)
}
function startPolling() {
stopPolling()
pollTimer = setInterval(() => {
syncFromDom()
}, 200)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
watch(
() => props.modelValue,
(val) => {
const next = normalize(val ?? '')
if (next !== localValue.value) {
localValue.value = next
if (inputRef.value) {
inputRef.value.value = next
}
}
},
{ immediate: true },
)
onMounted(async () => {
await nextTick()
beginSmsOtpCapture(props.length)
unsubscribeOtp = subscribeSmsOtp((code) => {
applyCode(code)
})
if (props.autofocus) {
focusInput()
}
startPolling()
})
onBeforeUnmount(() => {
unsubscribeOtp?.()
unsubscribeOtp = null
stopPolling()
// listener سراسری را abort نکن — دیالوگ Allow هنوز ممکن است باز باشد
})
defineExpose({
focusInput,
clearInput,
fillInput,
startWebOtp,
inputRef,
})
</script>
<template>
<div class="sms-otp" dir="ltr" @click="focusInput">
<div class="sms-otp__boxes" aria-hidden="true">
<div
v-for="(digit, index) in digits"
:key="index"
class="sms-otp__box otp-input"
:class="{
'sms-otp__box--filled': !!digit,
'sms-otp__box--active': localValue.length === index,
}"
>
{{ digit }}
</div>
</div>
<input
ref="inputRef"
class="sms-otp__input"
type="text"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
name="one-time-code"
:maxlength="length"
enterkeyhint="done"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
aria-label="کد تایید پیامک"
@input="onInput"
@change="onChange"
@keyup="onInput"
@paste="onInput"
@animationstart="onAnimationStart"
/>
</div>
</template>
<style scoped>
.sms-otp {
position: relative;
display: flex;
justify-content: center;
width: 100%;
margin-top: 1.25rem;
direction: ltr;
}
.sms-otp__boxes {
display: flex;
justify-content: center;
pointer-events: none;
}
.sms-otp__box {
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
margin: 0 8px;
font-size: 20px;
font-weight: 600;
border: 1px solid #d1d5db;
border-radius: 4px;
background: #fff;
color: #111827;
}
.sms-otp__box--active {
border-color: var(--App-primary-color, #0a1b37);
}
.sms-otp__input {
position: absolute;
inset: 0;
z-index: 2;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
/* کمتر از این را Chrome گاهی autofill نمی‌کند */
opacity: 0.15;
color: transparent;
caret-color: var(--App-primary-color, #0a1b37);
background: transparent;
font-size: 16px;
letter-spacing: 48px;
text-align: left;
direction: ltr;
animation-name: onAutoFillStart;
animation-duration: 0.001s;
}
@keyframes onAutoFillStart {
from {
/**/
}
to {
/**/
}
}
</style>
+93 -16
View File
@@ -1,39 +1,116 @@
import { persianToEnglishDigits } from '@/utils/persianDigits'
/** استخراج کد عددی از نتیجه WebOTP / Credential */
type OtpHandler = (code: string) => void
let activeController: AbortController | null = null
let listenGeneration = 0
let pendingCode: string | null = null
const handlers = new Set<OtpHandler>()
/** استخراج کد عددی از نتیجه WebOTP / Credential / رشته خام */
export function extractOtpCode(
credential: Credential | null | undefined,
length = 4
credential: Credential | null | undefined | string,
length = 4,
): string {
const raw = (credential as { code?: string } | null | undefined)?.code ?? ''
let raw = ''
if (typeof credential === 'string') {
raw = credential
} else if (credential && typeof credential === 'object') {
const c = credential as { code?: string; id?: string }
raw = c.code ?? c.id ?? ''
}
return persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, length)
}
/**
* گوش دادن به SMS از طریق WebOTP API (Chrome Android).
* با کلیک Allow روی دیالوگ مرورگر، onCode با کد ۴ رقمی صدا زده می‌شود.
*/
export function listenForSmsOtp(onCode: (code: string) => void): AbortController {
const ac = new AbortController()
function emitCode(code: string) {
pendingCode = code
handlers.forEach((handler) => {
try {
handler(code)
} catch {
/* ignore subscriber errors */
}
})
}
if (!('OTPCredential' in window) || !navigator.credentials?.get) {
return ac
/**
* شروع زودهنگام WebOTP — باید قبل از ارسال SMS / همزمان با درخواست کد صدا زده شود.
* اگر از قبل در حال listen باشد، دوباره abort نمی‌کند (دیالوگ Allow را خراب نکند).
*/
export function beginSmsOtpCapture(length = 4): void {
if (typeof window === 'undefined' || !navigator.credentials?.get) {
return
}
if (activeController && !activeController.signal.aborted) {
return
}
const ac = new AbortController()
activeController = ac
const generation = ++listenGeneration
void navigator.credentials
.get({
otp: { transport: ['sms'] },
signal: ac.signal,
} as CredentialRequestOptions)
.then((credential) => {
const code = extractOtpCode(credential, 4)
if (code.length === 4) {
onCode(code)
if (generation !== listenGeneration) return
const code = extractOtpCode(credential, length)
if (code.length === length) {
emitCode(code)
}
})
.catch(() => {
/* aborted or denied — ignore */
/* Deny / Abort / unsupported */
})
.finally(() => {
if (activeController === ac) {
activeController = null
}
})
}
/** عضویت برای دریافت کد — اگر کد از قبل آمده باشد همان لحظه می‌دهد */
export function subscribeSmsOtp(handler: OtpHandler): () => void {
handlers.add(handler)
if (pendingCode) {
try {
handler(pendingCode)
} catch {
/* ignore */
}
}
return () => {
handlers.delete(handler)
}
}
export function clearPendingSmsOtp(): void {
pendingCode = null
}
export function stopSmsOtpCapture(): void {
listenGeneration += 1
activeController?.abort()
activeController = null
}
/**
* سازگاری با کد قبلی — subscribe + begin
* توجه: abort کردن این controller دیگر listener سراسری را قطع نمی‌کند.
*/
export function listenForSmsOtp(onCode: (code: string) => void): AbortController {
const ac = new AbortController()
const unsubscribe = subscribeSmsOtp((code) => {
if (!ac.signal.aborted) {
onCode(code)
}
})
beginSmsOtpCapture()
ac.signal.addEventListener('abort', () => {
unsubscribe()
})
return ac
}
+24 -103
View File
@@ -1,15 +1,22 @@
import { computed, ref, watch } from 'vue'
import { getAllPrices } from '@/api/home'
import type { Product } from '@/models/products'
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
import { isKatodMesGroup, isMaftolMesGroup, isShamshMesGroup } from '@/utils/productDealMethod'
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
export type ChartMetalType = 'katod' | 'maftol' | 'shamsh'
type ChartPoint = [number, number]
/**
* مارک‌آپ ثابت روی قیمت بورس کالا برای نمودار:
* کاتد +۳٪ ، مفتول +۶٪ ، شمش +۱۲٪
*/
export const CHART_METAL_MARKUP_RATIO: Record<ChartMetalType, number> = {
katod: 0.03,
maftol: 0.06,
shamsh: 0.12,
}
/** Module-level chart state — one dataset shared by mobile and desktop PriceChart instances. */
const priceChartSeries = ref<{ name: string; data: ChartPoint[] }[]>([
{ name: 'قیمت', data: [] },
@@ -20,65 +27,18 @@ const chartLoading = ref(false)
const selectedChartMetal = ref<ChartMetalType>('katod')
const catalogProducts = getProductsCatalogRef()
function resolveChartProducts(products: Product[]) {
return {
katod: findKatodProduct(products),
maftol: findMaftolProduct(products),
shamsh: findShamshProduct(products),
}
}
const maftolChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.maftol)
const shamshChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.shamsh)
/** نسبت مفتول به کاتد — reactive به قیمت لحظه‌ای و کاتالوگ محصولات */
const maftolChartRatio = computed(() => {
void lastPrice.value?.price_last
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
return deriveMetalChartRatio(maftol, katod)
})
/** نسبت شمش مس به کاتد */
const shamshChartRatio = computed(() => {
void lastPrice.value?.price_last
const { katod, shamsh } = resolveChartProducts(catalogProducts.value)
return deriveMetalChartRatio(shamsh, katod)
})
const selectedMetalChartRatio = computed(() => {
if (selectedChartMetal.value === 'maftol') {
return maftolChartRatio.value
}
if (selectedChartMetal.value === 'shamsh') {
return shamshChartRatio.value
}
return 0
})
const selectedMetalChartRatio = computed(
() => CHART_METAL_MARKUP_RATIO[selectedChartMetal.value],
)
let chartFetchPromise: Promise<void> | null = null
let chartInitialized = false
let metalOffsetPromise: Promise<void> | null = null
let metalOffsetLoaded = false
function findKatodProduct(products: Product[]) {
return (
products.find((product) => isKatodMesGroup(product.group_type))
?? products.find((product) => (product.title ?? '').includes('کاتد'))
)
}
function findMaftolProduct(products: Product[]) {
return (
products.find((product) => isMaftolMesGroup(product.group_type))
?? products.find((product) => (product.title ?? '').includes('مفتول'))
)
}
function findShamshProduct(products: Product[]) {
return products.find((product) =>
isShamshMesGroup(product.group_type, product.title)
)
}
function applyChartRatio(basePrice: number, ratio: number) {
if (!ratio) {
return basePrice
@@ -87,7 +47,7 @@ function applyChartRatio(basePrice: number, ratio: number) {
return Math.round(basePrice + basePrice * ratio)
}
/** بالاترین/پایین‌ترین امروز — برای boursecu با نسبت مفتول/شمش نسبت به کاتد */
/** بالاترین/پایین‌ترین امروز — همان مارک‌آپ ثابت فلز روی قیمت بورس کالا */
export function resolveMetalDayRangePrices(options: {
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
metal: ChartMetalType
@@ -95,44 +55,14 @@ export function resolveMetalDayRangePrices(options: {
}) {
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
if (options.metal === 'katod') {
return { high: baseHigh, low: baseLow }
}
const ratio = options.metalRatio || CHART_METAL_MARKUP_RATIO[options.metal]
return {
high: applyChartRatio(baseHigh, options.metalRatio),
low: applyChartRatio(baseLow, options.metalRatio),
high: applyChartRatio(baseHigh, ratio),
low: applyChartRatio(baseLow, ratio),
}
}
function deriveMetalChartRatio(metalProduct?: Product, katodProduct?: Product) {
const metalOffset = resolvePriceOffsetRatio(metalProduct?.price_offset_percent)
if (metalOffset) {
return metalOffset
}
if (!metalProduct) {
return 0
}
const liveBuy = getTradeBuyPrice(lastPrice.value)
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
const metalLive = getProductBuyPrice(metalProduct)
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
const metalStatic = Number(metalProduct.price ?? 0) + Number(metalProduct.price_profit ?? 0)
const referencePrice = katodLive || liveBuy || katodStatic
const derivedMetalPrice = metalLive || metalStatic
if (!referencePrice || !derivedMetalPrice || referencePrice === derivedMetalPrice) {
return 0
}
return (derivedMetalPrice - referencePrice) / referencePrice
}
function normalizeChartTimestamp(timestamp: number) {
return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp
}
@@ -165,15 +95,9 @@ function buildChartPoints(items: Array<{ timestamp: number; value: number }>): C
function syncChartSeries() {
const ratio = selectedMetalChartRatio.value
const data =
selectedChartMetal.value !== 'katod'
? rawChartPoints.value.map(
([timestamp, value]) =>
[timestamp, applyChartRatio(value, ratio)] as ChartPoint
)
: rawChartPoints.value
priceChartSeries.value[0].data = data
priceChartSeries.value[0].data = rawChartPoints.value.map(
([timestamp, value]) => [timestamp, applyChartRatio(value, ratio)] as ChartPoint,
)
}
watch([selectedChartMetal, maftolChartRatio, shamshChartRatio], () => {
@@ -242,10 +166,7 @@ function updateChartData(timeline: string) {
function setChartMetal(metal: ChartMetalType) {
selectedChartMetal.value = metal
if (metal !== 'katod' && !catalogProducts.value.length) {
void loadMetalChartOffsets(true)
}
syncChartSeries()
}
export function usePriceChart() {
+19 -82
View File
@@ -118,31 +118,12 @@
name="token"
style="position: relative"
>
<OtpInput
<SmsOtpField
ref="refOtpInput"
:is-input-num="true"
:num-inputs="4"
:should-auto-focus="true"
autocomplete="one-time-code"
:class="variant === 'desktop' ? 'dauth-otp justify-center' : 'mt-5 justify-center'"
input-classes="otp-input"
separator=""
style="direction: ltr"
:value="userFormState.token || ''"
@on-change="handleOnChange"
@on-complete="handleOnComplete"
/>
<!-- برای WebOTP / پیشنهاد کیبورد: autocomplete باید روی input واقعی باشد -->
<input
class="web-otp-assist"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="4"
tabindex="-1"
aria-hidden="true"
:value="userFormState.token || ''"
@input="onWebOtpAssistInput"
v-model="userFormState.token"
:class="variant === 'desktop' ? 'dauth-otp' : undefined"
@change="handleOnChange"
@complete="handleOnComplete"
/>
</AFormItem>
@@ -333,8 +314,8 @@ import { mobileValidation, required } from '@/utils/inputRules'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { authenticateWithFingerprint, getToken, loading, removeToken, setToken, URLUserType } from '@/utils'
import {computed, reactive, ref, inject, unref, nextTick, onBeforeUnmount} from 'vue'
import OtpInput from 'vue3-otp-input'
import {computed, reactive, ref, inject, unref} from 'vue'
import SmsOtpField from '@/components/SmsOtpField.vue'
import type { Login } from '@/models/login'
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
@@ -357,8 +338,8 @@ import {
isTWA
} from "@/utils/config";
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
import { normalizeAuthFormFields, persianToEnglishDigits } from '@/utils/persianDigits'
import { listenForSmsOtp } from '@/utils/webOtp'
import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { beginSmsOtpCapture, clearPendingSmsOtp } from '@/utils/webOtp'
import {getConfig} from "@/api/config";
import shareAppleIcon from '@/assets/img/share-apple.png'
import addToHomeScreenIcon from '@/assets/img/Add-to-homeScreen.png'
@@ -390,8 +371,7 @@ const walletStore = useWalletStore()
const setting=useSettingsStore()
const data = reactive(['ورود با پیامک', 'ورود با کلمه عبور'])
const value = ref(data[0])
const refOtpInput = ref<InstanceType<typeof OtpInput> | null>(null)
let otpAbort: AbortController | null = null
const refOtpInput = ref<InstanceType<typeof SmsOtpField> | null>(null)
const userFormState = reactive<Login>({
provider: 'customer'
@@ -402,18 +382,16 @@ onFinish(() => {
enableSendAgainCode.value = true
})
onBeforeUnmount(() => {
otpAbort?.abort()
otpAbort = null
})
async function reSendCode() {
userFormState.token = undefined
;(refOtpInput.value as { clearInput?: () => void } | null)?.clearInput?.()
clearPendingSmsOtp()
refOtpInput.value?.clearInput?.()
enableSendAgainCode.value = false
normalizeAuthFormFields(userFormState)
beginSmsOtpCapture()
await loginCustomer(userFormState)
getcode()
refOtpInput.value?.startWebOtp?.()
refOtpInput.value?.focusInput?.()
reset()
}
@@ -489,10 +467,11 @@ function phoneVerification() {
userFormState.token = undefined
userFormState.password = undefined
userFormState.provider = 'customer'
// مهم: قبل از ارسال SMS گوش بده تا دیالوگ Allow به listener زنده وصل باشد
beginSmsOtpCapture()
const { id } = await loginCustomer(userFormState)
userFormState.id = id
stepTwoVerify.value = true
getcode()
reset()
} else {
const { access_token } = await verifyToken(userFormState)
@@ -510,37 +489,6 @@ function phoneVerification() {
}
})
}
const getcode = () => {
otpAbort?.abort()
otpAbort = listenForSmsOtp((code) => {
void applySmsOtpCode(code)
})
}
async function applySmsOtpCode(code: string) {
userFormState.token = code
await nextTick()
const input = refOtpInput.value as { fillInput?: (v: string) => void } | null
if (input?.fillInput) {
try {
input.fillInput(code)
return
} catch {
/* fall through */
}
}
await phoneVerification()
}
function onWebOtpAssistInput(event: Event) {
const raw = (event.target as HTMLInputElement | null)?.value ?? ''
const code = persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, 4)
if (code.length === 4) {
void applySmsOtpCode(code)
} else {
userFormState.token = code
}
}
const goToEducate = () => {
router.push({ name: "educate" })
@@ -570,18 +518,7 @@ async function proceedWithInstall() {
transform: translate(100%, 0);
}
/* Must stay in DOM (not display:none) so Chrome/Safari can autofill OTP */
.web-otp-assist {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
opacity: 0;
pointer-events: none;
.error-token {
position: relative;
}
</style>
+12 -35
View File
@@ -1,7 +1,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import OtpInput from 'vue3-otp-input'
import type SmsOtpField from '@/components/SmsOtpField.vue'
import type { Login } from '@/models/login'
import {
authenticateWithFingerprint,
@@ -11,14 +11,13 @@ import {
} from '@/utils'
import useCountdownTimer from '@/utils/useCountdownTimer'
import { loginCustomer, verifyToken } from '@/api/login'
import router from '@/router'
import { useUserStore } from '@/store/userStore'
import { useWalletStore } from '@/store/walletStore'
import { useSettingsStore } from '@/store/settingsStore'
import { setTruth } from '@/app/authBoot'
import { completeLoginNavigation } from '@/layouts/composables/useMobileScrollReset'
import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { listenForSmsOtp } from '@/utils/webOtp'
import { beginSmsOtpCapture, clearPendingSmsOtp } from '@/utils/webOtp'
export function useLoginForm() {
const { time, onFinish, reset } = useCountdownTimer('00:02:00', 'mm:ss')
@@ -27,11 +26,10 @@ export function useLoginForm() {
const formRef = ref<FormInstance>()
const userStore = useUserStore()
const walletStore = useWalletStore()
const refOtpInput = ref<InstanceType<typeof OtpInput> | null>(null)
const refOtpInput = ref<InstanceType<typeof SmsOtpField> | null>(null)
const isSMS = ref(true)
const isRegister = ref<boolean | undefined>()
const showPWAInstallGuide = ref(false)
let otpAbort: AbortController | null = null
const userFormState = reactive<Login>({
provider: 'customer',
@@ -42,17 +40,19 @@ export function useLoginForm() {
})
onBeforeUnmount(() => {
otpAbort?.abort()
otpAbort = null
// SmsOtpField cleans its own listener
})
async function reSendCode() {
userFormState.token = undefined
;(refOtpInput.value as { clearInput?: () => void } | null)?.clearInput?.()
clearPendingSmsOtp()
refOtpInput.value?.clearInput?.()
enableSendAgainCode.value = false
normalizeAuthFormFields(userFormState)
beginSmsOtpCapture()
await loginCustomer(userFormState)
getcode()
refOtpInput.value?.startWebOtp?.()
refOtpInput.value?.focusInput?.()
reset()
}
@@ -92,31 +92,8 @@ export function useLoginForm() {
await completeLoginNavigation()
}
async function applySmsOtpCode(code: string) {
userFormState.token = code
await nextTick()
const input = refOtpInput.value as { fillInput?: (v: string) => void } | null
if (input?.fillInput) {
try {
input.fillInput(code)
// fillInput → on-complete → handleOnComplete → phoneVerification
return
} catch {
/* fall through */
}
}
await phoneVerification()
}
function getcode() {
otpAbort?.abort()
otpAbort = listenForSmsOtp((code) => {
void applySmsOtpCode(code)
})
}
function phoneVerification() {
formRef.value?.validateFields().then(async () => {
return formRef.value?.validateFields().then(async () => {
try {
loading.value = true
normalizeAuthFormFields(userFormState)
@@ -125,10 +102,10 @@ export function useLoginForm() {
userFormState.token = undefined
userFormState.password = undefined
userFormState.provider = 'customer'
beginSmsOtpCapture()
const { id } = await loginCustomer(userFormState)
userFormState.id = id
stepTwoVerify.value = true
getcode()
reset()
} else {
const { access_token } = await verifyToken(userFormState)
@@ -3,15 +3,13 @@ import { mobileValidation, required } from '@/utils/inputRules'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import OtpInput from 'vue3-otp-input'
import SmsOtpField from '@/components/SmsOtpField.vue'
import { useConfigStore } from '@/store/configStore'
import { useLoginPageContext } from '../../composables/useLoginPage'
import DesktopPwaGuideModal from './DesktopPwaGuideModal.vue'
import router from '@/router'
import {isTWA} from "@/utils/config";
import {useUserStore} from "@/store/userStore";
import {useSettingsStore} from "@/store/settingsStore";
import { persianToEnglishDigits } from '@/utils/persianDigits'
const emit = defineEmits<{
register: [val: boolean]
@@ -42,17 +40,6 @@ const {
proceedWithInstall,
} = useLoginPageContext().loginForm
function onWebOtpAssistInput(event: Event) {
const raw = (event.target as HTMLInputElement | null)?.value ?? ''
const code = persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, 4)
if (code.length === 4) {
userFormState.token = code
handleOnComplete(code)
} else {
handleOnChange(code)
}
}
function goToEducate() {
router.push({ name: 'educate' })
}
@@ -137,30 +124,12 @@ function goToAboutUs() {
name="token"
style="position: relative"
>
<OtpInput
<SmsOtpField
ref="refOtpInput"
:is-input-num="true"
:num-inputs="4"
:should-auto-focus="true"
autocomplete="one-time-code"
class="dauth-otp justify-center"
input-classes="otp-input"
separator=""
style="direction: ltr"
:value="userFormState.token || ''"
@on-change="handleOnChange"
@on-complete="handleOnComplete"
/>
<input
class="web-otp-assist"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="4"
tabindex="-1"
aria-hidden="true"
:value="userFormState.token || ''"
@input="onWebOtpAssistInput"
v-model="userFormState.token"
class="dauth-otp"
@change="handleOnChange"
@complete="handleOnComplete"
/>
</AFormItem>