5bc81d5b3b
iOS SwiftUI app with Supabase auth/realtime, Node.js backend, Docker/Supabase self-hosted infrastructure, and APNs scheduler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
3.8 KiB
Swift
125 lines
3.8 KiB
Swift
import SwiftUI
|
|
import Supabase
|
|
|
|
@MainActor
|
|
class AppState: ObservableObject {
|
|
@Published var isAuthenticated = false
|
|
@Published var currentUser: User?
|
|
@Published var windowState: WindowState = .closed
|
|
|
|
private var windowTimer: Timer?
|
|
|
|
enum WindowState { case closed, open, posted, missed }
|
|
|
|
init() {
|
|
Task { await checkSession() }
|
|
startWindowTimer()
|
|
observeAuthChanges()
|
|
}
|
|
|
|
// MARK: - Auth
|
|
|
|
func checkSession() async {
|
|
do {
|
|
let session = try await supabase.auth.session
|
|
isAuthenticated = true
|
|
await loadProfile(userId: session.user.id)
|
|
} catch {
|
|
isAuthenticated = false
|
|
}
|
|
}
|
|
|
|
func signIn(email: String, password: String) async throws {
|
|
try await supabase.signIn(email: email, password: password)
|
|
let session = try await supabase.auth.session
|
|
isAuthenticated = true
|
|
await loadProfile(userId: session.user.id)
|
|
}
|
|
|
|
func signIn(username: String, password: String) async throws {
|
|
try await supabase.signIn(username: username, password: password)
|
|
let session = try await supabase.auth.session
|
|
isAuthenticated = true
|
|
await loadProfile(userId: session.user.id)
|
|
}
|
|
|
|
func signUp(email: String, password: String, username: String, displayName: String) async throws {
|
|
try await supabase.signUp(email: email, password: password, username: username, displayName: displayName)
|
|
let session = try await supabase.auth.session
|
|
isAuthenticated = true
|
|
await loadProfile(userId: session.user.id)
|
|
}
|
|
|
|
func signOut() {
|
|
Task {
|
|
try? await supabase.auth.signOut()
|
|
}
|
|
isAuthenticated = false
|
|
currentUser = nil
|
|
}
|
|
|
|
func deleteAccount() async throws {
|
|
try await supabase.deleteAccount()
|
|
isAuthenticated = false
|
|
currentUser = nil
|
|
}
|
|
|
|
private func loadProfile(userId: UUID) async {
|
|
guard let profile = try? await supabase.getMyProfile() else { return }
|
|
currentUser = User(
|
|
id: profile.id.uuidString,
|
|
username: profile.username,
|
|
displayName: profile.displayName,
|
|
bio: profile.bio,
|
|
avatarURL: profile.avatarUrl.flatMap(URL.init),
|
|
followerCount: 0,
|
|
followingCount: 0,
|
|
postCount: 0,
|
|
isFollowing: false
|
|
)
|
|
}
|
|
|
|
private func observeAuthChanges() {
|
|
Task {
|
|
for await (event, session) in await supabase.auth.authStateChanges {
|
|
switch event {
|
|
case .signedIn:
|
|
if let session {
|
|
isAuthenticated = true
|
|
await loadProfile(userId: session.user.id)
|
|
}
|
|
case .signedOut, .userDeleted:
|
|
isAuthenticated = false
|
|
currentUser = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Window State
|
|
|
|
func updateWindowState() {
|
|
let hour = Calendar.current.component(.hour, from: Date())
|
|
guard hour >= 2 && hour < 5 else { windowState = .closed; return }
|
|
|
|
let hasPosted = UserDefaults.standard.object(forKey: "lastPostDate")
|
|
.flatMap { $0 as? Date }
|
|
.map { Calendar.current.isDateInToday($0) } ?? false
|
|
windowState = hasPosted ? .posted : .open
|
|
}
|
|
|
|
func markAsPosted() {
|
|
UserDefaults.standard.set(Date(), forKey: "lastPostDate")
|
|
updateWindowState()
|
|
}
|
|
|
|
private func startWindowTimer() {
|
|
updateWindowState()
|
|
windowTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
|
|
Task { @MainActor [weak self] in self?.updateWindowState() }
|
|
}
|
|
}
|
|
}
|