File size: 5,203 Bytes
00a912e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { storage } from '@/lib/storage'
import { useSocketContext } from '@/providers/socket-context'
import type { MusicSource, MyPlatformAuth, PlatformAuthStatus } from '@music-together/shared'
import { EVENTS, QR_STATUS } from '@music-together/shared'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'

/**
 * Hook:管理平台认证 UI 状态
 *
 * 职责:
 * - QR 扫码登录状态(二维码数据、扫码状态、加载状态)
 * - 平台认证状态(聚合状态、个人状态)
 * - 用户操作方法(requestQrCode、setCookie、logout)
 *
 * 不负责:
 * - Cookie 持久化(由 useAuthSync 处理)
 * - Cookie 验证结果 toast(由 useAuthSync 处理)
 */
export function useAuth() {
  const { socket } = useSocketContext()
  const [platformStatus, setPlatformStatus] = useState<PlatformAuthStatus[]>([])
  const [myStatus, setMyStatus] = useState<MyPlatformAuth[]>([])
  const [statusLoaded, setStatusLoaded] = useState(false)
  const [qrData, setQrData] = useState<{ key: string; qrimg: string } | null>(null)
  const [qrStatus, setQrStatus] = useState<{ status: number; message: string } | null>(null)
  const [isQrLoading, setIsQrLoading] = useState(false)
  const [isClaimingKugouConceptVip, setIsClaimingKugouConceptVip] = useState(false)
  const [qrPlatform, setQrPlatform] = useState<MusicSource>('netease')

  // Ref 跟踪最新的 qrPlatform,避免重建回调导致重启轮询
  const qrPlatformRef = useRef<MusicSource>(qrPlatform)
  const qrKeyRef = useRef<string | null>(null)
  const qrCompletedRef = useRef(false)

  useEffect(() => {
    const onStatusUpdate = (data: PlatformAuthStatus[]) => {
      setPlatformStatus(data)
    }

    const onMyStatus = (data: MyPlatformAuth[]) => {
      setMyStatus(data)
      setStatusLoaded(true)
    }

    const onQrGenerated = (data: { key: string; qrimg: string }) => {
      qrCompletedRef.current = false
      qrKeyRef.current = data.key
      setQrData(data)
      setQrStatus({ status: 801, message: '等待扫码' })
      setIsQrLoading(false)
    }

    const onQrStatus = (data: { status: number; message: string; key?: string }) => {
      if (qrCompletedRef.current) return
      if (data.key && data.key !== qrKeyRef.current) return
      if (data.status === QR_STATUS.SUCCESS || data.status === QR_STATUS.EXPIRED) {
        qrCompletedRef.current = true
        qrKeyRef.current = null
      }
      setQrStatus(data)
      if (data.status === QR_STATUS.SUCCESS || data.status === QR_STATUS.EXPIRED) {
        setIsQrLoading(false)
      }
    }

    const onKugouConceptVipClaim = (data: { success: boolean; message: string }) => {
      setIsClaimingKugouConceptVip(false)
      if (data.success) toast.success(data.message)
      else toast.error(data.message)
    }

    socket.on(EVENTS.AUTH_STATUS_UPDATE, onStatusUpdate)
    socket.on(EVENTS.AUTH_MY_STATUS, onMyStatus)
    socket.on(EVENTS.AUTH_QR_GENERATED, onQrGenerated)
    socket.on(EVENTS.AUTH_QR_STATUS, onQrStatus)
    socket.on(EVENTS.AUTH_CLAIM_KUGOU_CONCEPT_VIP_RESULT, onKugouConceptVipClaim)

    // 挂载时拉取当前状态(覆盖延迟挂载场景)
    socket.emit(EVENTS.AUTH_GET_STATUS)

    return () => {
      socket.off(EVENTS.AUTH_STATUS_UPDATE, onStatusUpdate)
      socket.off(EVENTS.AUTH_MY_STATUS, onMyStatus)
      socket.off(EVENTS.AUTH_QR_GENERATED, onQrGenerated)
      socket.off(EVENTS.AUTH_QR_STATUS, onQrStatus)
      socket.off(EVENTS.AUTH_CLAIM_KUGOU_CONCEPT_VIP_RESULT, onKugouConceptVipClaim)
    }
  }, [socket])

  const requestQrCode = useCallback(
    (platform: MusicSource) => {
      setQrData(null)
      qrCompletedRef.current = false
      qrKeyRef.current = null
      setQrStatus(null)
      setIsQrLoading(true)
      setQrPlatform(platform)
      qrPlatformRef.current = platform
      socket.emit(EVENTS.AUTH_REQUEST_QR, { platform })
    },
    [socket],
  )

  const checkQrStatus = useCallback(
    (key: string) => {
      if (qrCompletedRef.current || qrKeyRef.current !== key) return
      socket.emit(EVENTS.AUTH_CHECK_QR, { key, platform: qrPlatformRef.current })
    },
    [socket],
  )

  const setCookie = useCallback(
    (platform: MusicSource, cookie: string) => {
      socket.emit(EVENTS.AUTH_SET_COOKIE, { platform, cookie })
    },
    [socket],
  )

  const logout = useCallback(
    (platform: MusicSource) => {
      storage.removeAuthCookie(platform)
      socket.emit(EVENTS.AUTH_LOGOUT, { platform })
    },
    [socket],
  )

  const resetQr = useCallback(() => {
    qrCompletedRef.current = true
    qrKeyRef.current = null
    setQrData(null)
    setQrStatus(null)
    setIsQrLoading(false)
  }, [])

  const claimKugouConceptVip = useCallback(() => {
    if (isClaimingKugouConceptVip) return
    setIsClaimingKugouConceptVip(true)
    socket.emit(EVENTS.AUTH_CLAIM_KUGOU_CONCEPT_VIP)
  }, [isClaimingKugouConceptVip, socket])

  return {
    platformStatus,
    myStatus,
    statusLoaded,
    qrData,
    qrStatus,
    qrPlatform,
    isQrLoading,
    requestQrCode,
    checkQrStatus,
    setCookie,
    logout,
    resetQr,
    claimKugouConceptVip,
    isClaimingKugouConceptVip,
  }
}