import { ref } from "vue"; import { createRequestController, isRequestCancelled } from "@/services/api/request-controller.js"; import { authApi } from "@/services/api/auth-service.js"; import { createAuthSmsCooldown } from "@/utils/auth/sms-cooldown.js"; import { createTacRenderContext, isAuthPhone, isSmsDeliveryOutcomeUnknown, normalizeCaptchaRequirement, normalizeTacSuccess, } from "@/utils/auth/verification.js"; import { runtimeConfig } from "@/utils/runtime-config.js"; export const useSmsVerification = ({ operationCode, requestIdPrefix, phone, isActive, showFeedback, getErrorMessage = (error, fallback) => error?.message || fallback, }) => { if (!phone || typeof phone !== "object" || !("value" in phone)) { throw new TypeError("phone must be a Vue ref"); } if ( typeof isActive !== "function" || typeof showFeedback !== "function" || typeof getErrorMessage !== "function" ) { throw new TypeError("isActive, showFeedback and getErrorMessage must be functions"); } const tacVisible = ref(false); const tacContext = ref(null); const sendingCode = ref(false); const cooldownSeconds = ref(0); const sentPhone = ref(""); const captchaRequirementRequestController = createRequestController(); const smsDeliveryRequestController = createRequestController(); const smsCooldown = createAuthSmsCooldown({ operationCode, onChange: (seconds) => { cooldownSeconds.value = seconds; }, }); let requestSequence = 0; let disposed = false; const canCommit = () => !disposed && isActive(); const closeTac = () => { tacVisible.value = false; tacContext.value = null; }; const markSmsSent = (requestedPhone, message = "验证码已发送") => { if (!canCommit()) return; sentPhone.value = requestedPhone; smsCooldown.start(); showFeedback(message); }; const requestCode = async () => { if (sendingCode.value || cooldownSeconds.value > 0) return false; if (!isAuthPhone(phone.value)) return false; const requestedPhone = phone.value; let smsDeliveryStarted = false; sendingCode.value = true; try { const requirementResponse = await authApi.getCaptchaRequirement( { operationCode, subject: requestedPhone }, { requestController: captchaRequirementRequestController }, ); if (!canCommit()) return false; if (phone.value !== requestedPhone) { throw new Error("手机号已变化,请重新获取验证码"); } const requirement = normalizeCaptchaRequirement(requirementResponse); if (!requirement.required) { smsDeliveryStarted = true; await authApi.sendSmsCode( { operationCode, phone: requestedPhone }, { requestController: smsDeliveryRequestController }, ); markSmsSent(requestedPhone); return true; } requestSequence += 1; tacContext.value = createTacRenderContext({ requestId: `${requestIdPrefix}-${requestSequence}`, baseUrl: runtimeConfig.baseUrl, clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, operationCode, subject: requestedPhone, requirement, }); tacVisible.value = true; return true; } catch (error) { if (canCommit()) { if (smsDeliveryStarted && isSmsDeliveryOutcomeUnknown(error)) { markSmsSent( requestedPhone, "发送结果未知,如收到短信可直接填写;60 秒后可重试", ); } else if (!isRequestCancelled(error)) { showFeedback(getErrorMessage(error, "安全验证暂不可用")); } } return false; } finally { if (canCommit()) sendingCode.value = false; } }; const completeTac = async (result) => { const expectedContext = tacContext.value; if (!expectedContext) return false; try { const ticket = normalizeTacSuccess(result, expectedContext.requestId); if (phone.value !== expectedContext.subject) { throw new Error("手机号已变化,请重新验证"); } closeTac(); sendingCode.value = true; await authApi.sendSmsCode( { operationCode, phone: expectedContext.subject, validToken: ticket.validToken, }, { requestController: smsDeliveryRequestController }, ); markSmsSent(expectedContext.subject); return true; } catch (error) { closeTac(); if (canCommit()) { if (isSmsDeliveryOutcomeUnknown(error)) { markSmsSent( expectedContext.subject, "发送结果未知,如收到短信可直接填写;60 秒后可重试", ); } else if (!isRequestCancelled(error)) { showFeedback(getErrorMessage(error, "验证码发送失败")); } } return false; } finally { if (canCommit()) sendingCode.value = false; } }; const handleTacFailure = ({ message } = {}) => showFeedback(message || "行为验证未通过,请重试"); const handleTacError = ({ message } = {}) => { closeTac(); showFeedback(message || "安全验证暂不可用"); }; const cancel = () => { captchaRequirementRequestController.abort(); smsDeliveryRequestController.abort(); sendingCode.value = false; }; const dispose = () => { disposed = true; cancel(); closeTac(); smsCooldown.dispose(); }; return { tacVisible, tacContext, sendingCode, cooldownSeconds, sentPhone, requestCode, completeTac, closeTac, handleTacFailure, handleTacError, cancel, dispose, syncCooldown: smsCooldown.sync, }; };