Files
tianma-if cd4f010d8f feat(ios): add native SwiftUI client with Android UI parity
Replace the abandoned RN iOS path with a SwiftUI app (iOS 17+) that reuses
the shared TipTap editor bundle, GRDB local mirror/outbox, and Android-aligned
shell chrome for list, create, detail, and Me. Package the editor HTML in the
app resources so Markdown renders, present edit from the workspace root with a
UIKit FAB, and keep caret stable while typing.
2026-08-07 02:52:01 +08:00

87 lines
3.2 KiB
Swift

import Foundation
import Observation
@Observable
@MainActor
final class SessionStore {
private(set) var isLoading = true
private(set) var session: MobileSession?
private(set) var lastError: String?
let client: APIClient
private let keychainSessionKey = KeychainStore.Key.session
init(client: APIClient = APIClient(baseURL: URL(string: "https://localhost")!)) {
self.client = client
}
var isSignedIn: Bool { session != nil }
var dataScope: String? {
guard let session else { return nil }
guard let url = try? EdgeEverURLNormalizer.normalizeInstanceURL(session.baseUrl) else { return nil }
return SyncProtocol.createDataScope(baseURL: url, userId: session.user?.id)
}
func bootstrap() async {
defer { isLoading = false }
do {
guard let raw = try KeychainStore.string(forKey: keychainSessionKey),
let data = raw.data(using: .utf8)
else { return }
let stored = try EdgeEverJSON.decoder.decode(MobileSession.self, from: data)
let baseURL = try EdgeEverURLNormalizer.normalizeInstanceURL(stored.baseUrl)
await client.update(baseURL: baseURL, token: stored.token, onUnauthorized: { [weak self] in
Task { @MainActor in self?.clearLocalSession() }
})
session = stored
} catch {
lastError = error.localizedDescription
clearLocalSession()
}
}
func signIn(baseUrl: String, username: String, password: String) async throws {
lastError = nil
let url = try EdgeEverURLNormalizer.normalizeInstanceURL(baseUrl)
if url.scheme?.lowercased() == "http" {
// Allowed with local networking; UI should already warn.
}
let deviceId = try DeviceId.getOrCreate()
await client.update(baseURL: url, token: nil)
let auth = try await client.login(LoginInput(username: username, password: password, deviceId: deviceId))
guard auth.authenticated, let token = auth.sessionToken else {
throw APIError(
status: 400,
code: nil,
message: "登录成功但服务端没有返回移动端会话。请确认服务端已更新到支持 App 登录的版本。"
)
}
let next = MobileSession(baseUrl: url.absoluteString, token: token, user: auth.user)
try persist(next)
await client.update(baseURL: url, token: token, onUnauthorized: { [weak self] in
Task { @MainActor in self?.clearLocalSession() }
})
session = next
}
func signOut() async {
if session != nil {
try? await client.logout()
}
clearLocalSession()
}
private func persist(_ session: MobileSession) throws {
let data = try EdgeEverJSON.encoder.encode(session)
guard let raw = String(data: data, encoding: .utf8) else { return }
try KeychainStore.setString(raw, forKey: keychainSessionKey)
}
func clearLocalSession() {
try? KeychainStore.delete(forKey: keychainSessionKey)
session = nil
Task { await client.update(baseURL: URL(string: "https://localhost")!, token: nil) }
}
}