Initial commit: nightly iOS app + Supabase backend

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>
This commit is contained in:
denshooter
2026-04-23 23:31:38 +02:00
commit 5bc81d5b3b
80 changed files with 9958 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
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() }
}
}
}