fix bug of WebOTP

This commit is contained in:
2026-07-21 12:06:42 +02:00
parent a60f1fbc68
commit 3be79ef9f0
6 changed files with 429 additions and 171 deletions
+20 -83
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>
@@ -159,7 +140,7 @@
<div v-else class="flex items-center my-[23px] justify-center">
<span class="text-gray-400 mt-1">{{ userFormState.mobile }}</span>
<div class="flex items-center text-gray-700 mr-2" @click="stepTwoVerify = false">
<span class="ml-1">ویرایش شماره</span>
<span class="ml-1">ویرایش شش2ماره</span>
<BasicIcon icon="vuesax-linear:edit" size="20" />
</div>
</div>
@@ -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>