Files
nademitex_user/src/services/authService.ts
T
safavimohadeseh78andCursor f252749d18 feat(auth): referrer code at signup and agent code in profile
Optional referrer agent code in registration with field validation UX. Show read-only agent referral code in profile edit and desktop overview.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 10:38:16 +03:30

174 lines
4.5 KiB
TypeScript

import { extractNumericString, normalizePhone } from '@/utils/format'
import { apiRequest, ApiError, getAccessToken, setAccessToken } from './http'
import type { User } from '@/types'
const USER_KEY = 'ft_user'
const PENDING_OTP_KEY = 'ft_pending_otp'
type PendingOtp = {
phone: string
isRegister: boolean
firstName?: string
lastName?: string
referrerAgentCode?: string
}
let memoryPending: PendingOtp | null = null
function cacheUser(user: User | null): void {
if (user) localStorage.setItem(USER_KEY, JSON.stringify(user))
else localStorage.removeItem(USER_KEY)
}
export function getCachedUser(): User | null {
try {
const raw = localStorage.getItem(USER_KEY)
if (!raw) return null
return JSON.parse(raw) as User
} catch {
return null
}
}
export function getCurrentUser(): User | null {
if (!getAccessToken()) return null
return getCachedUser()
}
export function isAuthenticated(): boolean {
return !!getAccessToken()
}
function setPendingOtp(value: PendingOtp | null) {
memoryPending = value
try {
if (value) sessionStorage.setItem(PENDING_OTP_KEY, JSON.stringify(value))
else sessionStorage.removeItem(PENDING_OTP_KEY)
} catch {
// ignore
}
}
export async function sendOtp(
phone: string,
options: {
firstName?: string
lastName?: string
isRegister: boolean
referrerAgentCode?: string
},
): Promise<{ success: boolean; message: string }> {
const normalized = normalizePhone(phone)
if (!/^09\d{9}$/.test(normalized)) {
return { success: false, message: 'شماره موبایل معتبر نیست.' }
}
if (options.isRegister && (!options.firstName?.trim() || !options.lastName?.trim())) {
return { success: false, message: 'نام و نام خانوادگی الزامی است.' }
}
try {
const referrerAgentCode = options.referrerAgentCode?.trim() || undefined
setPendingOtp({
phone: normalized,
isRegister: options.isRegister,
firstName: options.firstName,
lastName: options.lastName,
referrerAgentCode,
})
const res = await apiRequest<{ success: boolean; message: string; debugCode?: string }>(
'/auth/otp/request',
{
method: 'POST',
auth: false,
body: {
phone: normalized,
firstName: options.firstName,
lastName: options.lastName,
isRegister: options.isRegister,
referrerAgentCode,
},
},
)
const hint = res.debugCode ? ` (کد: ${res.debugCode})` : ''
return { success: true, message: `${res.message}${hint}` }
} catch (e) {
return {
success: false,
message: e instanceof ApiError ? e.message : 'ارسال کد ناموفق بود.',
}
}
}
export async function verifyOtp(
phone: string,
code: string,
): Promise<{ success: boolean; message: string; user?: User }> {
const normalized = normalizePhone(phone)
try {
const res = await apiRequest<{ accessToken: string; user: User }>('/auth/otp/verify', {
method: 'POST',
auth: false,
body: { phone: normalized, code: extractNumericString(code) },
})
setAccessToken(res.accessToken)
cacheUser(res.user)
setPendingOtp(null)
return { success: true, message: 'ورود موفق.', user: res.user }
} catch (e) {
return {
success: false,
message: e instanceof ApiError ? e.message : 'تأیید کد ناموفق بود.',
}
}
}
export function logout(): void {
setAccessToken(null)
cacheUser(null)
setPendingOtp(null)
}
export async function fetchMe(): Promise<User | null> {
const token = getAccessToken()
if (!token) {
cacheUser(null)
return null
}
try {
const me = await apiRequest<User>('/auth/me')
cacheUser(me)
return me
} catch {
cacheUser(null)
return null
}
}
export async function updateProfile(
data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId' | 'avatarUrl' | 'address'>>,
): Promise<User | null> {
try {
const updated = await apiRequest<User>('/auth/profile', {
method: 'PATCH',
body: data,
})
cacheUser(updated)
return updated
} catch (e) {
const avatarOnly =
data.avatarUrl !== undefined &&
data.firstName === undefined &&
data.lastName === undefined &&
data.nationalId === undefined &&
data.address === undefined
const current = getCachedUser()
if (!avatarOnly || !current) throw e
const merged: User = { ...current }
if (data.avatarUrl) merged.avatarUrl = data.avatarUrl
else delete merged.avatarUrl
cacheUser(merged)
return merged
}
}