| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- //
- // LNAccountManager.swift
- // Lanu
- //
- // Created by OneeChan on 2025/11/6.
- //
- import Foundation
- import GoogleSignIn
- import AuthenticationServices
- protocol LNAccountManagerNotify {
- func onUserLogin()
- func onUserLogout()
-
- func onLoginCaptchaCoolDownChanged(time: Int)
- }
- extension LNAccountManagerNotify {
- func onUserLogin() {}
- func onUserLogout() {}
-
- func onLoginCaptchaCoolDownChanged(time: Int) { }
- }
- extension String {
- var isMyUid: Bool {
- myUid == self
- }
- }
- var myUid: String {
- LNAccountManager.shared.uid
- }
- var hasLogin: Bool {
- !myUid.isEmpty
- }
- class LNAccountManager: NSObject {
- static let shared = LNAccountManager()
-
- private let onlineHeartbeatInterval: TimeInterval = 5
- private var onlineHeartbeatTimer: Timer?
- private var isReportingOnlineHeartbeat = false
-
- private(set) var token = LNUserDefaults[.token, ""] {
- didSet { LNUserDefaults[.token] = token }
- }
- private(set) var uid: String = LNUserDefaults[.uid, ""] {
- didSet { LNUserDefaults[.uid] = uid }
- }
-
- var wasLogin: Bool {
- !token.isEmpty && !uid.isEmpty
- }
-
- private let captchaCoolDown = 60
- private var captchaRemain = 0
- private var captchaTimer: Timer?
- var canSendCaptcha: Bool {
- captchaRemain == 0
- }
-
- func doGoogleLogin(_ vc: UIViewController) {
- showLoading()
- GIDSignIn.sharedInstance.signIn(withPresenting: vc) { [weak self] result, err in
- guard let self else { return }
- guard err == nil,
- let result,
- let token = result.user.idToken?.tokenString else {
- dismissLoading()
- return
- }
- self.loginByGoogle(data: token) { _ in
- dismissLoading()
- }
- }
- }
-
- func doAppleLogin() {
- let provider = ASAuthorizationAppleIDProvider()
- let request = provider.createRequest()
- request.requestedScopes = [.fullName, .email]
- let controller = ASAuthorizationController(authorizationRequests: [request])
- controller.delegate = self
- // controller.presentationContextProvider = self
- showLoading()
- controller.performRequests()
- }
-
- private override init() {
- super.init()
-
- let clientID = if LNAppConfig.shared.curEnv == .test {
- "981655295954-noc65ii1gfgpq3mrc0r75t7gq66v57bj.apps.googleusercontent.com"
- } else {
- "955524882346-a7fs1l3798khu5hn058m0veqqcvli7h4.apps.googleusercontent.com"
- }
-
- GIDSignIn.sharedInstance.configuration = GIDConfiguration(clientID: clientID)
- }
- }
- extension LNAccountManager: ASAuthorizationControllerDelegate {
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithAuthorization authorization: ASAuthorization) {
- guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
- let token = credential.identityToken,
- let tokenStr = String(data: token, encoding: .utf8)
- else {
- dismissLoading()
- return
- }
-
- loginByApple(data: tokenStr) { _ in
- dismissLoading()
- }
- }
-
- func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
- dismissLoading()
- }
- }
- extension LNAccountManager {
- func loginByToken(handler: ((Bool) -> Void)? = nil) {
- LNHttpManager.shared.refreshToken { [weak self] res, err in
- guard let self else { return }
- guard err == nil, let res else {
- if case .serverError = err {
- self.clean()
- }
- showToast(err?.errorDesc)
- handler?(false)
- return
- }
- self.token = res.token
- handler?(true)
-
- self.notifyUserLogin()
- }
- }
-
- private func loginByGoogle(data: String, handler: ((Bool) -> Void)? = nil) {
- LNHttpManager.shared.loginByGoogle(token: data) { [weak self] response, err in
- guard let self else { return }
- guard err == nil, let response else {
- showToast(err?.errorDesc)
- handler?(false)
- self.clean()
- return
- }
- self.token = response.token
- self.uid = response.userProfile.userNo
- handler?(true)
-
- self.notifyUserLogin()
- LNStatisticManager.shared.reportRegister(method: .google)
- }
- }
-
- private func loginByApple(data: String, handler: ((Bool) -> Void)? = nil) {
- LNHttpManager.shared.loginByApple(token: data) { [weak self] response, err in
- guard let self else { return }
- guard err == nil, let response else {
- showToast(err?.errorDesc)
- handler?(false)
- self.clean()
- return
- }
- self.token = response.token
- self.uid = response.userProfile.userNo
- handler?(true)
-
- self.notifyUserLogin()
- LNStatisticManager.shared.reportRegister(method: .apple)
- }
- }
-
- func loginByPhone(code: String, num: String, captcha: String,
- handler: ((Bool) -> Void)? = nil) {
- LNHttpManager.shared.loginByPhone(code: code, num: num, captcha: captcha)
- { [weak self] response, err in
- guard let self else { return }
- guard err == nil, let response else {
- showToast(err?.errorDesc)
- handler?(false)
- self.clean()
- return
- }
- self.token = response.token
- self.uid = response.userProfile.userNo
- handler?(true)
-
- self.notifyUserLogin()
- LNStatisticManager.shared.reportRegister(method: .phone)
- }
- }
-
- func logout() {
- LNHttpManager.shared.logout { [weak self] err in
- guard let self else { return }
- guard err == nil else {
- return
- }
- self.clean()
- }
- }
-
- #if DEBUG
- func loginByEmail(email: String, completion: @escaping (Bool) -> Void) {
- LNHttpManager.shared.loginByEmail(email: email) { [weak self] response, err in
- guard let self else { return }
- guard err == nil, let response else {
- showToast(err?.errorDesc)
- completion(false)
- self.clean()
- return
- }
- self.token = response.token
- self.uid = response.userProfile.userNo
- completion(true)
-
- self.notifyUserLogin()
- }
- }
- #endif
- }
- extension LNAccountManager {
- func getLoginCaptcha(code: String, phone: String,
- queue: DispatchQueue = .main,
- handler: @escaping (Bool) -> Void) {
- LNHttpManager.shared.getLoginCaptcha(code: code, phone: phone) { [weak self] err in
- queue.asyncIfNotGlobal {
- handler(err == nil)
- }
- guard let self else { return }
- if let err {
- showToast(err.errorDesc)
- } else {
- captchaRemain = captchaCoolDown
- notifyCaptchaTime(time: captchaRemain)
- startCaptchaTimer()
- }
- }
- }
-
- private func startCaptchaTimer() {
- runOnMain { [weak self] in
- guard let self else { return }
- stopCaptchaTimer()
-
- let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
- guard let self else { return }
- captchaRemain -= 1
- notifyCaptchaTime(time: captchaRemain)
- if captchaRemain == 0 {
- stopCaptchaTimer()
- }
- }
- RunLoop.main.add(timer, forMode: .common)
- captchaTimer = timer
- }
- }
-
- private func stopCaptchaTimer() {
- captchaTimer?.invalidate()
- captchaTimer = nil
- }
- }
- extension LNAccountManager {
- func clean() {
- let wasLogin = !token.isEmpty
- token = ""
- uid = ""
-
- if wasLogin {
- notifyUserLogout()
- }
- }
- }
- extension LNAccountManager {
- private func startOnlineHeartbeatTimerIfNeed() {
- runOnMain { [weak self] in
- guard let self else { return }
- guard onlineHeartbeatTimer == nil else { return }
- let timer = Timer.scheduledTimer(withTimeInterval: onlineHeartbeatInterval, repeats: true)
- { [weak self] _ in
- guard let self else { return }
- guard wasLogin && LNAppConfig.shared.isForeground else {
- return
- }
- isReportingOnlineHeartbeat = true
- LNHttpManager.shared.reportOnlineHeartbeat { [weak self] err in
- guard let self else { return }
- isReportingOnlineHeartbeat = false
- if let err {
- Log.d("report online heartbeat failed: \(err.errorDesc)")
- }
- }
- }
- RunLoop.main.add(timer, forMode: .common)
- onlineHeartbeatTimer = timer
- Log.d("start online heartbeat")
- }
- }
-
- private func stopOnlineHeartbeatTimer() {
- runOnMain { [weak self] in
- guard let self else { return }
- onlineHeartbeatTimer?.invalidate()
- onlineHeartbeatTimer = nil
- Log.d("stop online heartbeat")
- }
- }
- }
- extension LNAccountManager {
- private func notifyUserLogin() {
- startOnlineHeartbeatTimerIfNeed()
- LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogin() }
- }
- private func notifyUserLogout() {
- stopOnlineHeartbeatTimer()
- LNEventDeliver.notifyEvent { ($0 as? LNAccountManagerNotify)?.onUserLogout() }
- }
-
- private func notifyCaptchaTime(time: Int) {
- LNEventDeliver.notifyEvent {
- ($0 as? LNAccountManagerNotify)?.onLoginCaptchaCoolDownChanged(time: time)
- }
- }
- }
|