fix(user): register dialog, KYC modal, and home loading gaps

Repair gift-card register success modal text, enforce KYC with correct identity checks, and show skeleton/fallback while home layout loads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-10 11:48:28 +03:30
co-authored by Cursor
parent 2977b479dd
commit d6a2caffe0
7 changed files with 116 additions and 63 deletions
@@ -1,5 +1,8 @@
# یادداشت‌های یادگیری — abshode-domain-lookup # یادداشت‌های یادگیری — abshode-domain-lookup
- 2026-11-10 | مدال موفقیت ثبت‌نام خالی | `DialogVantOneButton.vue` | تم لاگین `color:#fff`؛ بدون teleport متن سفید روی دیالوگ سفید — مثل `DialogVant` با teleport+رنگ تیره
- 2026-11-10 | خانه خالی بدون KYC | `RequiredKycModal` + `HomeLayoutRenderer` | v-model:open روی computed فقط‌خواندنی؛ تشخیص احراز با `isCustomerIdentityVerified`؛ skeleton هنگام lazy layout
فرمت هر خط: فرمت هر خط:
- YYYY-MM-DD | موضوع | مسیر فایل(ها) | نکتهٔ ۱ خطی - YYYY-MM-DD | موضوع | مسیر فایل(ها) | نکتهٔ ۱ خطی
+5 -1
View File
@@ -169,6 +169,7 @@ import {
useChatService, useChatService,
} from '@/utils/chat' } from '@/utils/chat'
import { getToken } from '@/utils' import { getToken } from '@/utils'
import { isCustomerIdentityVerified } from '@/utils/customerIdentity'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { showBrowserNotification } from '@/utils/browserNotification' import { showBrowserNotification } from '@/utils/browserNotification'
@@ -329,7 +330,10 @@ watch(
const userStore = useUserStore() const userStore = useUserStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const isKycRequired = computed( const isKycRequired = computed(
() => Boolean(getToken()) && (userStore.userInfo?.is_legal === 0 || !userStore.userInfo?.is_legal), () =>
Boolean(getToken()) &&
Boolean(userStore.userInfo) &&
!isCustomerIdentityVerified(userStore.userInfo?.is_legal),
) )
const giftCardFabAccent = computed( const giftCardFabAccent = computed(
+2 -1
View File
@@ -8,6 +8,7 @@ import { useAuthStore } from '@/store/useAuthStore'
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset' import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
import { getFingerPosition, setFingerPosition } from '@/app/authBoot' import { getFingerPosition, setFingerPosition } from '@/app/authBoot'
import { isGuestHomeQuery } from '@/utils/guestHome' import { isGuestHomeQuery } from '@/utils/guestHome'
import { isCustomerIdentityVerified } from '@/utils/customerIdentity'
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationNormalized } from 'vue-router'
const PUBLIC_ROUTE_NAMES = ['login', '404', 'educate', 'aboutUs', 'aboutSoftware', 'tvMonitor'] as const const PUBLIC_ROUTE_NAMES = ['login', '404', 'educate', 'aboutUs', 'aboutSoftware', 'tvMonitor'] as const
@@ -85,7 +86,7 @@ export function setupRouterGuards() {
// کاربر احراز هویت نشده نباید بتواند به صفحات داخلی دسترسی پیدا کند // کاربر احراز هویت نشده نباید بتواند به صفحات داخلی دسترسی پیدا کند
if ( if (
userStore.userInfo && userStore.userInfo &&
(userStore.userInfo.is_legal === 0 || !userStore.userInfo.is_legal) && !isCustomerIdentityVerified(userStore.userInfo.is_legal) &&
!isPublicRoute(to) && !isPublicRoute(to) &&
to.name !== 'home' to.name !== 'home'
) { ) {
+58 -48
View File
@@ -1,61 +1,71 @@
<template> <template>
<!-- <div v-if="modelValue">--> <van-dialog
<van-dialog class="oneButtonDialog"> v-model:show="show"
<div class="my-4"> class="oneButtonDialog"
teleport="body"
:close-on-click-overlay="closeOnClickOverlay"
:z-index="zIndex"
v-bind="$attrs"
>
<div class="oneButtonDialog__body my-4 text-slate-800">
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
<slot name="img">{{ text2 }}</slot> <slot name="img">{{ text2 }}</slot>
</div> </div>
<div class="w-full flex justify-center text-[18px] font-semibold"> <div class="w-full flex justify-center text-[18px] font-semibold text-slate-800">
<slot name="title">{{ text1 }}</slot> <slot name="title">{{ text1 }}</slot>
</div> </div>
<div class=" border-t border-solid border-[#E5E7EB] pt-4 mx-2 mt-6 font-semibold text-[15px]"> <div
class="border-t border-solid border-[#E5E7EB] pt-4 mx-2 mt-6 font-semibold text-[15px] text-slate-800"
>
<slot name="button">{{ text3 }}</slot> <slot name="button">{{ text3 }}</slot>
</div> </div>
</div> </div>
</van-dialog> </van-dialog>
<!-- <van-dialog style="background-color: #2b3f6e" show-cancel-button cancelButtonText="بیخیال" confirmButtonText="بله" cancelButtonColor="">-->
<!-- <div class="w-full mt-6 text-white ">-->
<!-- <div class="w-full flex mb-[10px] ">-->
<!-- <Icon icon="vuesax-linear:info-circle" class="mx-4" size="25"/>-->
<!-- <span class="text-lg flex-1">-->
<!-- <slot name="title">{{text1}}</slot>-->
<!-- </span>-->
<!-- </div>-->
<!-- <div class="flex justify-start mb-6 mt-8" style="font-weight:700;">-->
<!-- <slot name="button">{{text2}}</slot>-->
<!-- </div>-->
<!-- </div>-->
<!-- </van-dialog>-->
<!-- </div>-->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue'
import { APP_Z_MODAL } from '@/utils/zIndex'
// import Icon from "/@/assets/icons/Icon.vue"; defineOptions({ inheritAttrs: false })
import {ref, watch} from 'vue';
const show = ref(true); const props = withDefaults(
const props = withDefaults(defineProps<{ defineProps<{
// modelValue: boolean, show?: boolean
timeOut: number, timeOut?: number
text1?: string, text1?: string
text2?: string, text2?: string
text3?: string text3?: string
// top?: string, closeOnClickOverlay?: boolean
zIndex?: number
}>(),
{
show: false,
timeOut: 3000,
closeOnClickOverlay: true,
zIndex: APP_Z_MODAL,
},
)
}>(), {}) const emit = defineEmits<{
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>() (e: 'update:show', value: boolean): void
watch(() => props.modelValue, (value) => { }>()
if (props.timeOut !== -1) showNotify()
const show = computed({
get: () => props.show,
set: (value: boolean) => emit('update:show', value),
}) })
const showNotify = () => {
console.log('notify', show.value) watch(
setTimeout(() => { () => props.show,
emit('update:modelValue', false) (value) => {
}, props.timeOut); if (value && props.timeOut !== -1) {
}; setTimeout(() => {
emit('update:show', false)
}, props.timeOut)
}
},
)
</script> </script>
<style lang="less"> <style lang="less">
@@ -67,17 +77,13 @@ const showNotify = () => {
animation: notify-in 0.2s reverse; animation: notify-in 0.2s reverse;
} }
.oneButtonDialog .van-dialog__confirm, .van-dialog__cancel { .oneButtonDialog .van-dialog__confirm,
.oneButtonDialog .van-dialog__cancel {
display: none !important; display: none !important;
} }
.van-dialog__confirm, .van-dialog__cancel {
//background-color: ;
}
.oneButtonDialog .van-dialog__confirm .van-button__text { .oneButtonDialog .van-dialog__confirm .van-button__text {
color: #1C2D56; color: #1c2d56;
font-weight: 600; font-weight: 600;
font-size: 15px; font-size: 15px;
} }
@@ -87,4 +93,8 @@ const showNotify = () => {
font-weight: 600; font-weight: 600;
font-size: 15px; font-size: 15px;
} }
.oneButtonDialog__body {
color: #1e293b;
}
</style> </style>
+27 -12
View File
@@ -1,13 +1,14 @@
<template> <template>
<a-modal <a-modal
v-model:open="isModalVisible" :open="requiresKyc"
:closable="false" :closable="false"
:mask-closable="false" :mask-closable="false"
:keyboard="false" :keyboard="false"
:footer="null" :footer="null"
:centered="true" :centered="true"
:z-index="5500" :z-index="APP_Z_MODAL"
:width="modalWidth" :width="modalWidth"
:get-container="getModalContainer"
class="required-kyc-modal" class="required-kyc-modal"
wrap-class-name="required-kyc-modal-wrap" wrap-class-name="required-kyc-modal-wrap"
> >
@@ -206,7 +207,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@@ -221,6 +222,8 @@ import { isValidPersian, requireNationalCode, required } from '@/utils/inputRule
import { normalizeAuthFormFields } from '@/utils/persianDigits' import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { performUserLogout } from '@/utils/authSession' import { performUserLogout } from '@/utils/authSession'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout' import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { isCustomerIdentityVerified } from '@/utils/customerIdentity'
import { APP_Z_MODAL, getModalContainer } from '@/utils/zIndex'
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
@@ -241,22 +244,22 @@ const formState = reactive({
const modalWidth = computed(() => (isDesktop.value ? '460px' : '92vw')) const modalWidth = computed(() => (isDesktop.value ? '460px' : '92vw'))
const isModalVisible = computed(() => { const requiresKyc = computed(() => {
if (!getToken()) return false if (!getToken()) return false
if (route.name === 'login' || route.name === 'tvMonitor' || route.name === 'aboutSoftware') { if (route.name === 'login' || route.name === 'tvMonitor' || route.name === 'aboutSoftware') {
return false return false
} }
const info = userStore.userInfo const info = userStore.userInfo
if (!info) return false if (!info) return false
// 0 indicates unverified. 1 and 2 indicate verified customer. return !isCustomerIdentityVerified(info.is_legal)
return info.is_legal === 0 || !info.is_legal
}) })
const isPendingApproval = computed(() => { const isPendingApproval = computed(() => {
const info = userStore.userInfo const info = userStore.userInfo
if (!info) return false if (!info) return false
// If national code is set and user is still unverified (is_legal === 0) return Boolean(
return Boolean(info.n_code && info.is_legal === 0) String(info.n_code ?? '').trim() && !isCustomerIdentityVerified(info.is_legal),
)
}) })
function populateFormFromUser() { function populateFormFromUser() {
@@ -295,8 +298,7 @@ async function onSubmit() {
const res: any = await updateMe(payload) const res: any = await updateMe(payload)
await userStore.getMyInfo() await userStore.getMyInfo()
const currentLegal = userStore.userInfo?.is_legal if (isCustomerIdentityVerified(userStore.userInfo?.is_legal)) {
if (currentLegal === 1 || currentLegal === 2) {
message.success('احراز هویت شما با موفقیت انجام و تأیید شد.') message.success('احراز هویت شما با موفقیت انجام و تأیید شد.')
isEditing.value = false isEditing.value = false
} else { } else {
@@ -317,8 +319,7 @@ async function onCheckStatus() {
try { try {
checkingStatus.value = true checkingStatus.value = true
await userStore.getMyInfo() await userStore.getMyInfo()
const currentLegal = userStore.userInfo?.is_legal if (isCustomerIdentityVerified(userStore.userInfo?.is_legal)) {
if (currentLegal === 1 || currentLegal === 2) {
message.success('حساب کاربری شما تأیید شد!') message.success('حساب کاربری شما تأیید شد!')
} else { } else {
message.info('اطلاعات شما همچنان در صف بررسی و تایید مدیریت است.') message.info('اطلاعات شما همچنان در صف بررسی و تایید مدیریت است.')
@@ -333,8 +334,22 @@ async function onCheckStatus() {
async function onLogout() { async function onLogout() {
await performUserLogout() await performUserLogout()
} }
onMounted(() => {
if (getToken() && !userStore.userInfo) {
void userStore.getMyInfo()
}
})
</script> </script>
<style lang="less">
.required-kyc-modal-wrap {
.ant-modal-content {
color: #1e293b;
}
}
</style>
<style scoped> <style scoped>
.kyc-modal-content { .kyc-modal-content {
font-family: inherit; font-family: inherit;
@@ -8,6 +8,7 @@ import HomeSkeleton from '../components/HomeSkeleton.vue'
import { getHomeLayoutConfig } from './registry' import { getHomeLayoutConfig } from './registry'
import { HOME_SECTION_COMPONENTS } from './sections' import { HOME_SECTION_COMPONENTS } from './sections'
import type { HomeLayoutConfig, HomeSectionId } from './types' import type { HomeLayoutConfig, HomeSectionId } from './types'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
const props = defineProps<{ const props = defineProps<{
layoutConfig?: HomeLayoutConfig layoutConfig?: HomeLayoutConfig
@@ -43,6 +44,11 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
function resolveSectionComponent(sectionId: HomeSectionId) { function resolveSectionComponent(sectionId: HomeSectionId) {
return HOME_SECTION_COMPONENTS[sectionId] return HOME_SECTION_COMPONENTS[sectionId]
} }
async function retryLoad() {
if (!getToken()) return
await userStore.getMyInfo()
}
</script> </script>
<template> <template>
@@ -77,4 +83,13 @@ function resolveSectionComponent(sectionId: HomeSectionId) {
</div> </div>
<HomeSkeleton v-else-if="showSkeleton" class="w-full" /> <HomeSkeleton v-else-if="showSkeleton" class="w-full" />
<div v-else class="home-layout-fallback flex flex-col items-center justify-center gap-3 p-6 text-center min-h-[50vh]">
<p class="text-sm text-slate-600 leading-7">
در حال آماده‌سازی صفحه اصلی هستیم. اگر چیزی نمایش داده نمی‌شود، یک‌بار تلاش مجدد کنید.
</p>
<BasicButton type="primary" class="!rounded-xl" @click="retryLoad">
تلاش مجدد
</BasicButton>
</div>
</template> </template>
+6 -1
View File
@@ -1,6 +1,7 @@
import { defineAsyncComponent, type Component } from 'vue' import { defineAsyncComponent, type Component } from 'vue'
import type { HomeLayoutConfig, HomeLayoutId } from './types' import type { HomeLayoutConfig, HomeLayoutId } from './types'
import { boursecuHomeLayoutConfig } from './configs/boursecu' import { boursecuHomeLayoutConfig } from './configs/boursecu'
import HomeSkeleton from '../components/HomeSkeleton.vue'
export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'boursecu' export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'boursecu'
@@ -12,7 +13,11 @@ let cachedLayoutRenderer: Component | null = null
function getLayoutRenderer(): Component { function getLayoutRenderer(): Component {
if (!cachedLayoutRenderer) { if (!cachedLayoutRenderer) {
cachedLayoutRenderer = defineAsyncComponent(() => import('./HomeLayoutRenderer.vue')) cachedLayoutRenderer = defineAsyncComponent({
loader: () => import('./HomeLayoutRenderer.vue'),
loadingComponent: HomeSkeleton,
delay: 0,
})
} }
return cachedLayoutRenderer return cachedLayoutRenderer