LNAccountManager.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. //
  2. // LNAccountManager.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/6.
  6. //
  7. import Foundation
  8. import GoogleSignIn
  9. import AuthenticationServices
  10. protocol LNAccountManagerNotify {
  11. func onUserLogin()
  12. func onUserLogout()
  13. func onLoginCaptchaCoolDownChanged(time: Int)
  14. }
  15. extension LNAccountManagerNotify {
  16. func onUserLogin() {}
  17. func onUserLogout() {}
  18. func onLoginCaptchaCoolDownChanged(time: Int) { }
  19. }
  20. extension String {
  21. var isMyUid: Bool {
  22. myUid == self
  23. }
  24. }
  25. var myUid: String {
  26. LNAccountManager.shared.uid
  27. }
  28. var hasLogin: Bool {
  29. !myUid.isEmpty
  30. }
  31. class LNAccountManager: NSObject {
  32. static let shared = LNAccountManager()
  33. private let onlineHeartbeatInterval: TimeInterval = 5
  34. private var onlineHeartbeatTimer: Timer?
  35. private var isReportingOnlineHeartbeat = false
  36. private(set) var token = LNUserDefaults[.token, ""] {
  37. didSet { LNUserDefaults[.token] = token }
  38. }
  39. private(set) var uid: String = LNUserDefaults[.uid, ""] {
  40. didSet { LNUserDefaults[.uid] = uid }
  41. }
  42. var wasLogin: Bool {
  43. !token.isEmpty && !uid.isEmpty
  44. }
  45. private let captchaCoolDown = 60
  46. private var captchaRemain = 0
  47. private var captchaTimer: Timer?
  48. var canSendCaptcha: Bool {
  49. captchaRemain == 0
  50. }
  51. func doGoogleLogin(_ vc: UIViewController) {
  52. showLoading()
  53. GIDSignIn.sharedInstance.signIn(withPresenting: vc) { [weak self] result, err in
  54. guard let self else { return }
  55. guard err == nil,
  56. let result,
  57. let token = result.user.idToken?.tokenString else {
  58. dismissLoading()
  59. return
  60. }
  61. self.loginByGoogle(data: token) { _ in
  62. dismissLoading()
  63. }
  64. }
  65. }
  66. func doAppleLogin() {
  67. let provider = ASAuthorizationAppleIDProvider()
  68. let request = provider.createRequest()
  69. request.requestedScopes = [.fullName, .email]
  70. let controller = ASAuthorizationController(authorizationRequests: [request])
  71. controller.delegate = self
  72. // controller.presentationContextProvider = self
  73. showLoading()
  74. controller.performRequests()
  75. }
  76. private override init() {
  77. super.init()
  78. let clientID = if LNAppConfig.shared.curEnv == .test {
  79. "981655295954-noc65ii1gfgpq3mrc0r75t7gq66v57bj.apps.googleusercontent.com"
  80. } else {
  81. "955524882346-a7fs1l3798khu5hn058m0veqqcvli7h4.apps.googleusercontent.com"
  82. }
  83. GIDSignIn.sharedInstance.configuration = GIDConfiguration(clientID: clientID)
  84. }
  85. }
  86. extension LNAccountManager: ASAuthorizationControllerDelegate {
  87. func authorizationController(controller: ASAuthorizationController,
  88. didCompleteWithAuthorization authorization: ASAuthorization) {
  89. guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
  90. let token = credential.identityToken,
  91. let tokenStr = String(data: token, encoding: .utf8)
  92. else {
  93. dismissLoading()
  94. return
  95. }
  96. loginByApple(data: tokenStr) { _ in
  97. dismissLoading()
  98. }
  99. }
  100. func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
  101. dismissLoading()
  102. }
  103. }
  104. extension LNAccountManager {
  105. func loginByToken(handler: ((Bool) -> Void)? = nil) {
  106. LNHttpManager.shared.refreshToken { [weak self] res, err in
  107. guard let self else { return }
  108. guard err == nil, let res else {
  109. if case .serverError = err {
  110. self.clean()
  111. }
  112. showToast(err?.errorDesc)
  113. handler?(false)
  114. return
  115. }
  116. self.token = res.token
  117. handler?(true)
  118. self.notifyUserLogin()
  119. }
  120. }
  121. private func loginByGoogle(data: String, handler: ((Bool) -> Void)? = nil) {
  122. LNHttpManager.shared.loginByGoogle(token: data) { [weak self] response, err in
  123. guard let self else { return }
  124. guard err == nil, let response else {
  125. showToast(err?.errorDesc)
  126. handler?(false)
  127. self.clean()
  128. return
  129. }
  130. self.token = response.token
  131. self.uid = response.userProfile.userNo
  132. handler?(true)
  133. self.notifyUserLogin()
  134. LNStatisticManager.shared.reportRegister(method: .google)
  135. }
  136. }
  137. private func loginByApple(data: String, handler: ((Bool) -> Void)? = nil) {
  138. LNHttpManager.shared.loginByApple(token: data) { [weak self] response, err in
  139. guard let self else { return }
  140. guard err == nil, let response else {
  141. showToast(err?.errorDesc)
  142. handler?(false)
  143. self.clean()
  144. return
  145. }
  146. self.token = response.token
  147. self.uid = response.userProfile.userNo
  148. handler?(true)
  149. self.notifyUserLogin()
  150. LNStatisticManager.shared.reportRegister(method: .apple)
  151. }
  152. }
  153. func loginByPhone(code: String, num: String, captcha: String,
  154. handler: ((Bool) -> Void)? = nil) {
  155. LNHttpManager.shared.loginByPhone(code: code, num: num, captcha: captcha)
  156. { [weak self] response, err in
  157. guard let self else { return }
  158. guard err == nil, let response else {
  159. showToast(err?.errorDesc)
  160. handler?(false)
  161. self.clean()
  162. return
  163. }
  164. self.token = response.token
  165. self.uid = response.userProfile.userNo
  166. handler?(true)
  167. self.notifyUserLogin()
  168. LNStatisticManager.shared.reportRegister(method: .phone)
  169. }
  170. }
  171. func logout() {
  172. LNHttpManager.shared.logout { [weak self] err in
  173. guard let self else { return }
  174. guard err == nil else {
  175. return
  176. }
  177. self.clean()
  178. }
  179. }
  180. #if DEBUG
  181. func loginByEmail(email: String, completion: @escaping (Bool) -> Void) {
  182. LNHttpManager.shared.loginByEmail(email: email) { [weak self] response, err in
  183. guard let self else { return }
  184. guard err == nil, let response else {
  185. showToast(err?.errorDesc)
  186. completion(false)
  187. self.clean()
  188. return
  189. }
  190. self.token = response.token
  191. self.uid = response.userProfile.userNo
  192. completion(true)
  193. self.notifyUserLogin()
  194. }
  195. }
  196. #endif
  197. }
  198. extension LNAccountManager {
  199. func getLoginCaptcha(code: String, phone: String,
  200. queue: DispatchQueue = .main,
  201. handler: @escaping (Bool) -> Void) {
  202. LNHttpManager.shared.getLoginCaptcha(code: code, phone: phone) { [weak self] err in
  203. queue.asyncIfNotGlobal {
  204. handler(err == nil)
  205. }
  206. guard let self else { return }
  207. if let err {
  208. showToast(err.errorDesc)
  209. } else {
  210. captchaRemain = captchaCoolDown
  211. notifyCaptchaTime(time: captchaRemain)
  212. startCaptchaTimer()
  213. }
  214. }
  215. }
  216. private func startCaptchaTimer() {
  217. runOnMain { [weak self] in
  218. guard let self else { return }
  219. stopCaptchaTimer()
  220. let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
  221. guard let self else { return }
  222. captchaRemain -= 1
  223. notifyCaptchaTime(time: captchaRemain)
  224. if captchaRemain == 0 {
  225. stopCaptchaTimer()
  226. }
  227. }
  228. RunLoop.main.add(timer, forMode: .common)
  229. captchaTimer = timer
  230. }
  231. }
  232. private func stopCaptchaTimer() {
  233. captchaTimer?.invalidate()
  234. captchaTimer = nil
  235. }
  236. }
  237. extension LNAccountManager {
  238. func clean() {
  239. let wasLogin = !token.isEmpty
  240. token = ""
  241. uid = ""
  242. if wasLogin {
  243. notifyUserLogout()
  244. }
  245. }
  246. }
  247. extension LNAccountManager {
  248. private func startOnlineHeartbeatTimerIfNeed() {
  249. runOnMain { [weak self] in
  250. guard let self else { return }
  251. guard onlineHeartbeatTimer == nil else { return }
  252. let timer = Timer.scheduledTimer(withTimeInterval: onlineHeartbeatInterval, repeats: true)
  253. { [weak self] _ in
  254. guard let self else { return }
  255. guard wasLogin && LNAppConfig.shared.isForeground else {
  256. return
  257. }
  258. isReportingOnlineHeartbeat = true
  259. LNHttpManager.shared.reportOnlineHeartbeat { [weak self] err in
  260. guard let self else { return }
  261. isReportingOnlineHeartbeat = false
  262. if let err {
  263. Log.d("report online heartbeat failed: \(err.errorDesc)")
  264. }
  265. }
  266. }
  267. RunLoop.main.add(timer, forMode: .common)
  268. onlineHeartbeatTimer = timer
  269. Log.d("start online heartbeat")
  270. }
  271. }
  272. private func stopOnlineHeartbeatTimer() {
  273. runOnMain { [weak self] in
  274. guard let self else { return }
  275. onlineHeartbeatTimer?.invalidate()
  276. onlineHeartbeatTimer = nil
  277. Log.d("stop online heartbeat")
  278. }
  279. }
  280. }
  281. extension LNAccountManager {
  282. private func notifyUserLogin() {
  283. startOnlineHeartbeatTimerIfNeed()
  284. LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogin() }
  285. }
  286. private func notifyUserLogout() {
  287. stopOnlineHeartbeatTimer()
  288. LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogout() }
  289. }
  290. private func notifyCaptchaTime(time: Int) {
  291. LNEventDeliver.notifyEvent {
  292. ($0 as? LNAccountManagerNotify)?.onLoginCaptchaCoolDownChanged(time: time)
  293. }
  294. }
  295. }