Files
My-KOPKB/fe/src/modules/profile/components/PhoneVerificationSection.vue
T
ismailmasseran b05e074456 Feature/phone register (#11)
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #11
2026-07-14 12:03:22 +08:00

185 lines
5.5 KiB
Vue

<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
getPhoneVerificationErrorMessage,
sendAuthenticatedPhoneVerificationOtp,
verifyAuthenticatedPhoneVerificationOtp,
} from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const loading = ref(false)
const changingPhone = ref(false)
const otpSent = ref(false)
const phoneNumber = ref(authStore.user?.phone_number ?? '')
const otp = ref('')
const feedbackMessage = ref('')
const errorMessage = ref('')
const isVerified = computed(() => authStore.isPhoneVerified)
const sectionDescription = computed(() => {
if (changingPhone.value) {
return 'Masukkan nombor telefon baharu dan sahkan dengan kod OTP SMS.'
}
if (isVerified.value) {
return 'Nombor telefon anda telah disahkan.'
}
return 'Sahkan nombor telefon anda melalui kod OTP SMS.'
})
watch(
() => authStore.user?.phone_number,
(value) => {
if (!otpSent.value && !changingPhone.value) {
phoneNumber.value = value ?? ''
}
},
)
const resetOtpState = () => {
otp.value = ''
otpSent.value = false
clearMessages()
}
const clearMessages = () => {
feedbackMessage.value = ''
errorMessage.value = ''
}
const handlePhoneInput = () => {
phoneNumber.value = phoneNumber.value.replace(/[^\d+]/g, '')
otp.value = ''
otpSent.value = false
}
const handleOtpInput = () => {
otp.value = otp.value.replace(/\D/g, '').slice(0, 6)
}
const startChangePhone = () => {
changingPhone.value = true
phoneNumber.value = ''
resetOtpState()
}
const cancelChangePhone = () => {
changingPhone.value = false
phoneNumber.value = authStore.user?.phone_number ?? ''
resetOtpState()
}
const handleSendOtp = async () => {
loading.value = true
clearMessages()
try {
const response = await sendAuthenticatedPhoneVerificationOtp({
phone_number: phoneNumber.value,
})
feedbackMessage.value = response.message
otpSent.value = true
} catch (error) {
errorMessage.value = getPhoneVerificationErrorMessage(error)
} finally {
loading.value = false
}
}
const handleVerifyOtp = async () => {
loading.value = true
clearMessages()
try {
const response = await verifyAuthenticatedPhoneVerificationOtp({
phone_number: phoneNumber.value,
otp: otp.value,
})
authStore.setUserProfile(response.data)
phoneNumber.value = response.data.phone_number ?? phoneNumber.value
changingPhone.value = false
otp.value = ''
otpSent.value = false
feedbackMessage.value = response.message
} catch (error) {
errorMessage.value = getPhoneVerificationErrorMessage(error)
} finally {
loading.value = false
}
}
</script>
<template>
<Box raised="single" class="p-6">
<div class="mb-6">
<h3 class="text-lg font-semibold text-slate-900">Nombor Telefon</h3>
<p class="mt-1 text-sm text-slate-500">
{{ sectionDescription }}
</p>
</div>
<div v-if="isVerified && !changingPhone" class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<Input id="profile-phone-verified" :model-value="authStore.user?.phone_number ?? '-'" type="tel" disabled
class="max-w-sm" />
<Badge variant="success">Disahkan</Badge>
</div>
<p v-if="feedbackMessage" class="text-sm text-slate-600">{{ feedbackMessage }}</p>
<Button type="button" look="outline" :disabled="loading" @click="startChangePhone">
Tukar Nombor Telefon
</Button>
</div>
<div v-else class="space-y-4">
<FieldGroup>
<Field class="max-w-sm">
<FieldLabel for="profile-phone-verify">No. Telefon</FieldLabel>
<Input id="profile-phone-verify" v-model="phoneNumber" type="tel" inputmode="tel"
placeholder="No. Telefon, contoh: 0123456790" autocomplete="tel" :disabled="loading || otpSent"
@input="handlePhoneInput" />
</Field>
<Field v-if="otpSent" class="max-w-sm">
<FieldLabel for="profile-phone-otp">Kod OTP</FieldLabel>
<Input id="profile-phone-otp" v-model="otp" type="text" inputmode="numeric" maxlength="6" placeholder="000000"
autocomplete="one-time-code" class="text-center tracking-[0.4em]" :disabled="loading"
@input="handleOtpInput" />
</Field>
</FieldGroup>
<p v-if="feedbackMessage" class="text-sm text-slate-600">{{ feedbackMessage }}</p>
<p v-if="errorMessage" class="text-sm text-danger">{{ errorMessage }}</p>
<div class="flex flex-wrap gap-2">
<Button v-if="!otpSent" type="button" variant="primary" :disabled="loading || !phoneNumber"
@click="handleSendOtp">
{{ loading ? 'Menghantar...' : 'Hantar Kod OTP' }}
</Button>
<template v-else>
<Button type="button" variant="primary" :disabled="loading || otp.length !== 6" @click="handleVerifyOtp">
{{ loading ? 'Mengesahkan...' : 'Sahkan OTP' }}
</Button>
<Button type="button" look="outline" :disabled="loading" @click="handleSendOtp">
Hantar Semula OTP
</Button>
</template>
<Button v-if="changingPhone" type="button" look="outline" :disabled="loading" @click="cancelChangePhone">
Batal
</Button>
</div>
</div>
</Box>
</template>