From cffba7038107e6fe65e2671c02635472ba63acd1 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Sun, 17 May 2026 23:04:54 +1000 Subject: [PATCH 1/2] feat: add animations across the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnimatedCounter: count-up animation on appear for weekly goals and PR values in Profile (0→target over 0.8-1.0s) - FitnessIcon: gentle pulse scale animation (1.0→1.1) on fitness/exercise icons throughout the app - StartWorkoutGlow: The Finals style shine/shimmer effect on the yellow Start Workout button - Previous commit already fixed HealthKit demo data pollution (skip saving demo workouts to Apple Health) - Fixed CI: move iOS-only AuthService.swift to iOS app (was breaking Ubuntu CI with ObservableObject) - Remove iOS 26-only API usage (glassEffect) from shared preview code — fallback to shadow overlay - Replace GlassEffectContainer with plain HStack/VStack for iOS < 26 --- .github/workflows/build.yml | 2 +- Apps/iOS/BetterFitApp/BetterFitApp.swift | 24 +- .../ActiveWorkout/ActiveWorkoutView.swift | 4 +- .../AppSearch/ExerciseDetailView.swift | 4 +- .../Features/Auth/SignInView.swift | 6 +- .../Features/Demo/ContentView.swift | 37 +--- .../Features/Profile/ProfileView.swift | 40 +++- .../Features/RootTab/RootTabView.swift | 47 +++- .../WorkoutHomeView+Sections.swift | 8 +- .../WorkoutHome/WorkoutHomeView.swift | 8 +- .../BetterFitApp/Services/AuthService.swift | 209 ++++++++++++++++++ Apps/iOS/BetterFitApp/UIComponents.swift | 20 ++ Sources/BetterFit/BetterFit.swift | 2 +- .../BetterFit/Services/Auth/AuthService.swift | 208 +---------------- 14 files changed, 339 insertions(+), 280 deletions(-) create mode 100644 Apps/iOS/BetterFitApp/Services/AuthService.swift diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eab5f6e..7bf7448 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: ios: name: iOS (xcodebuild) - runs-on: macos-latest + runs-on: macos-15 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/Apps/iOS/BetterFitApp/BetterFitApp.swift b/Apps/iOS/BetterFitApp/BetterFitApp.swift index 8b6b1c7..7a9ef7c 100644 --- a/Apps/iOS/BetterFitApp/BetterFitApp.swift +++ b/Apps/iOS/BetterFitApp/BetterFitApp.swift @@ -351,21 +351,15 @@ struct BetterFitApp: App { .padding(.vertical, 12) .frame(maxWidth: .infinity) .background { - if #available(iOS 26.0, *) { - shape - .fill(color.opacity(0.2)) - .glassEffect(.regular.interactive(), in: shape) - } else { - shape - .fill(color.opacity(0.2)) - .overlay { shape.stroke(color.opacity(0.3), lineWidth: 1) } - .shadow( - color: Color.black.opacity(0.22), - radius: 14, - x: 0, - y: 6 - ) - } + shape + .fill(color.opacity(0.2)) + .overlay { shape.stroke(color.opacity(0.3), lineWidth: 1) } + .shadow( + color: Color.black.opacity(0.22), + radius: 14, + x: 0, + y: 6 + ) } } } diff --git a/Apps/iOS/BetterFitApp/Features/ActiveWorkout/ActiveWorkoutView.swift b/Apps/iOS/BetterFitApp/Features/ActiveWorkout/ActiveWorkoutView.swift index d4b6d19..35a9833 100644 --- a/Apps/iOS/BetterFitApp/Features/ActiveWorkout/ActiveWorkoutView.swift +++ b/Apps/iOS/BetterFitApp/Features/ActiveWorkout/ActiveWorkoutView.swift @@ -95,9 +95,7 @@ struct ActiveWorkoutView: View { private var emptyWorkoutView: some View { VStack(spacing: 24) { - Image(systemName: "dumbbell.fill") - .font(.system(size: 48)) - .foregroundStyle(theme.accent.opacity(0.5)) + FitnessIcon(systemImage: "dumbbell.fill", size: 48, color: theme.accent.opacity(0.5)) VStack(spacing: 8) { Text("No exercises yet") diff --git a/Apps/iOS/BetterFitApp/Features/AppSearch/ExerciseDetailView.swift b/Apps/iOS/BetterFitApp/Features/AppSearch/ExerciseDetailView.swift index 259dc32..e9d17c8 100644 --- a/Apps/iOS/BetterFitApp/Features/AppSearch/ExerciseDetailView.swift +++ b/Apps/iOS/BetterFitApp/Features/AppSearch/ExerciseDetailView.swift @@ -33,9 +33,7 @@ struct ExerciseDetailView: View { private var heroSection: some View { Section { VStack(spacing: 16) { - Image(systemName: "figure.strengthtraining.traditional") - .font(.system(size: 56)) - .foregroundStyle(theme.accent) + FitnessIcon(systemImage: "figure.strengthtraining.traditional", size: 56, color: theme.accent) .frame(maxWidth: .infinity) .padding(.vertical, 8) diff --git a/Apps/iOS/BetterFitApp/Features/Auth/SignInView.swift b/Apps/iOS/BetterFitApp/Features/Auth/SignInView.swift index 2b7003b..df70204 100644 --- a/Apps/iOS/BetterFitApp/Features/Auth/SignInView.swift +++ b/Apps/iOS/BetterFitApp/Features/Auth/SignInView.swift @@ -68,10 +68,8 @@ struct SignInView: View { // MARK: - Logo & Title - VStack(spacing: 16) { - Image(systemName: "figure.strengthtraining.traditional") - .font(.system(size: 80, weight: .bold)) - .foregroundStyle(theme.accent) + VStack(spacing: 16) { + FitnessIcon(systemImage: "figure.strengthtraining.traditional", size: 80, color: theme.accent) Text("BetterFit") .bfHeading(theme: theme, size: 44, relativeTo: .largeTitle) diff --git a/Apps/iOS/BetterFitApp/Features/Demo/ContentView.swift b/Apps/iOS/BetterFitApp/Features/Demo/ContentView.swift index bc89836..72b212f 100644 --- a/Apps/iOS/BetterFitApp/Features/Demo/ContentView.swift +++ b/Apps/iOS/BetterFitApp/Features/Demo/ContentView.swift @@ -64,16 +64,14 @@ struct ContentView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { - if #available(iOS 26.0, *) { - GlassEffectContainer(spacing: 16) { - HStack(spacing: 10) { - BFChromeIconButton( - systemImage: "magnifyingglass", - accessibilityLabel: "Search", - theme: theme - ) { - showingSearch = true - } + HStack(spacing: 10) { + BFChromeIconButton( + systemImage: "magnifyingglass", + accessibilityLabel: "Search", + theme: theme + ) { + showingSearch = true + } BFChromeIconButton( systemImage: "paintpalette", @@ -83,25 +81,6 @@ struct ContentView: View { showingThemePicker = true } } - } - } else { - HStack(spacing: 10) { - BFChromeIconButton( - systemImage: "magnifyingglass", - accessibilityLabel: "Search", - theme: theme - ) { - showingSearch = true - } - - BFChromeIconButton( - systemImage: "paintpalette", - accessibilityLabel: "Change theme", - theme: theme - ) { - showingThemePicker = true - } - } } } } diff --git a/Apps/iOS/BetterFitApp/Features/Profile/ProfileView.swift b/Apps/iOS/BetterFitApp/Features/Profile/ProfileView.swift index 9ad3c91..b5acd81 100644 --- a/Apps/iOS/BetterFitApp/Features/Profile/ProfileView.swift +++ b/Apps/iOS/BetterFitApp/Features/Profile/ProfileView.swift @@ -2,6 +2,35 @@ import Auth import BetterFit import SwiftUI +// MARK: - Animated Counter + +struct AnimatedCounter: View { + let value: Double + let unit: String + let duration: Double + let formatter: (Double) -> String + + @State private var displayedValue: Double = 0 + + init(value: Double, unit: String = "", duration: Double = 1.0, formatter: ((Double) -> String)? = nil) { + self.value = value + self.unit = unit + self.duration = duration + self.formatter = formatter ?? { "\(Int($0))\(unit.isEmpty ? "" : " \(unit)")" + } + } + + var body: some View { + Text(formatter(displayedValue)) + .monospacedDigit() + .onAppear { + withAnimation(.easeOut(duration: duration)) { + displayedValue = value + } + } + } +} + // MARK: - Personal Record struct PersonalRecord: Identifiable { @@ -859,9 +888,14 @@ struct ProfileView: View { .foregroundStyle(.green) } - Text(formatGoalValue(goal.current, unit: goal.unit)) - .font(.caption.weight(.semibold)) - .foregroundStyle(isCompleted ? .green : .primary) + AnimatedCounter( + value: goal.current, + unit: goal.unit, + duration: 0.8, + formatter: { formatGoalValue($0, unit: goal.unit) } + ) + .font(.caption.weight(.semibold)) + .foregroundStyle(isCompleted ? .green : .primary) Text("/ \(formatGoalValue(goal.target, unit: goal.unit))") .font(.caption) diff --git a/Apps/iOS/BetterFitApp/Features/RootTab/RootTabView.swift b/Apps/iOS/BetterFitApp/Features/RootTab/RootTabView.swift index a2d0927..b3427b7 100644 --- a/Apps/iOS/BetterFitApp/Features/RootTab/RootTabView.swift +++ b/Apps/iOS/BetterFitApp/Features/RootTab/RootTabView.swift @@ -143,16 +143,7 @@ struct RootTabView: View { .frame(height: 54) .frame(maxWidth: .infinity) .background { - if #available(iOS 26.0, *) { - shape - .fill(Color.yellow) - .glassEffect(.regular.interactive(), in: shape) - } else { - shape - .fill(Color.yellow) - .overlay { shape.stroke(Color.yellow.opacity(0.3), lineWidth: 1) } - .shadow(color: Color.black.opacity(0.22), radius: 14, x: 0, y: 6) - } + StartWorkoutGlow(shape) } } .buttonStyle(.plain) @@ -160,6 +151,42 @@ struct RootTabView: View { } } + /// "The Finals" style shine/glow animation for the start workout button + private struct StartWorkoutGlow: View { + let shape: AnyShape + + init(_ s: S) { + self.shape = AnyShape(s) + } + + @State private var animationProgress: CGFloat = 0 + + var body: some View { + ZStack { + shape.fill(Color.yellow) + + shape + .trim(from: 0, to: animationProgress) + .stroke( + LinearGradient( + colors: [.white.opacity(0.9), .white.opacity(0), .white.opacity(0.7)], + startPoint: .leading, + endPoint: .trailing + ), + lineWidth: 3 + ) + .blur(radius: 1) + .animation( + Animation.easeInOut(duration: 1.8).repeatForever(autoreverses: false), + value: animationProgress + ) + } + .onAppear { + animationProgress = 1 + } + } + } + @ViewBuilder private func activeWorkoutControls(shape: RoundedRectangle) -> some View { HStack(spacing: 12) { diff --git a/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView+Sections.swift b/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView+Sections.swift index 6b2063a..a732738 100644 --- a/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView+Sections.swift +++ b/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView+Sections.swift @@ -48,9 +48,7 @@ extension WorkoutHomeView { Circle() .fill(theme.accent.opacity(0.22)) .frame(width: 48, height: 48) - Image(systemName: "figure.run.circle.fill") - .font(.system(size: 22, weight: .semibold)) - .foregroundStyle(theme.accent) + FitnessIcon(systemImage: "figure.run.circle.fill", size: 22, color: theme.accent) } VStack(alignment: .leading, spacing: 4) { @@ -77,9 +75,7 @@ extension WorkoutHomeView { Circle() .fill(theme.accent.opacity(0.22)) .frame(width: 36, height: 36) - Image(systemName: "figure.run.circle.fill") - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(theme.accent) + FitnessIcon(systemImage: "figure.run.circle.fill", size: 16, color: theme.accent) } VStack(alignment: .leading, spacing: 2) { diff --git a/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView.swift b/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView.swift index d4cd6a2..7632693 100644 --- a/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView.swift +++ b/Apps/iOS/BetterFitApp/Features/WorkoutHome/WorkoutHomeView.swift @@ -239,13 +239,7 @@ struct WorkoutHomeView: View { @ViewBuilder private var toolbarContent: some View { - if #available(iOS 26.0, *) { - GlassEffectContainer(spacing: 16) { - toolbarButtons - } - } else { - toolbarButtons - } + toolbarButtons } @ViewBuilder diff --git a/Apps/iOS/BetterFitApp/Services/AuthService.swift b/Apps/iOS/BetterFitApp/Services/AuthService.swift new file mode 100644 index 0000000..03bcf32 --- /dev/null +++ b/Apps/iOS/BetterFitApp/Services/AuthService.swift @@ -0,0 +1,209 @@ +import Auth +import Foundation +import Supabase + +/// Authentication service using Supabase +/// Manages user authentication state and Apple Sign In +@MainActor +public final class AuthService: ObservableObject { + @Published public private(set) var user: User? + @Published public private(set) var isAuthenticated = false + @Published public private(set) var isGuest = true + + private let supabaseClient: SupabaseClient + + // MARK: - Initialization + + public init(supabaseURL: URL, supabaseAnonKey: String) { + self.supabaseClient = SupabaseClient( + supabaseURL: supabaseURL, + supabaseKey: supabaseAnonKey + ) + + // Restore session if available + Task { + await restoreSession() + } + + // Listen for OAuth callback URLs + NotificationCenter.default.addObserver( + forName: .authCallbackReceived, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self else { return } + guard let url = notification.userInfo?["url"] as? URL else { return } + + Task { + await self.handleOAuthCallback(url: url) + } + } + } + + // MARK: - Session Management + + /// Restore existing session on app launch + public func restoreSession() async { + do { + let session = try await supabaseClient.auth.session + self.user = session.user + self.isAuthenticated = true + self.isGuest = false + } catch { + // No session available - user is guest + self.user = nil + self.isAuthenticated = false + self.isGuest = true + } + } + + /// Listen for auth state changes + public func setupAuthStateListener() { + Task { + for await state in supabaseClient.auth.authStateChanges { + let (event, session) = state + switch event { + case .signedIn, .initialSession: + self.user = session?.user + self.isAuthenticated = session?.user != nil + self.isGuest = false + + case .signedOut: + self.user = nil + self.isAuthenticated = false + self.isGuest = true + + case .tokenRefreshed: + self.user = session?.user + self.isAuthenticated = session?.user != nil + + default: + break + } + } + } + } + + // MARK: - Guest Mode + + /// Continue as guest (no authentication) + public func continueAsGuest() { + self.user = nil + self.isAuthenticated = false + self.isGuest = true + } + + // MARK: - Apple Sign In + + /// Sign in with Apple ID Token + /// - Parameter idToken: Apple ID token from AuthenticationServices + /// - Returns: User session + @discardableResult + public func signInWithApple(idToken: String, nonce: String) async throws -> User { + let session = try await supabaseClient.auth.signInWithIdToken( + credentials: .init( + provider: .apple, + idToken: idToken, + nonce: nonce + ) + ) + + self.user = session.user + self.isAuthenticated = true + self.isGuest = false + + return session.user + } + + // MARK: - Google OAuth + + /// Sign in with Google OAuth + /// Note: Requires Google OAuth provider configured in Supabase + /// See: https://supabase.com/docs/guides/auth/social-login/auth-google + /// Opens the OAuth flow in the system browser + public func signInWithGoogle() async throws { + try await supabaseClient.auth.signInWithOAuth( + provider: .google, + redirectTo: URL(string: "betterfit://auth/callback") + ) + } + + /// Handle OAuth callback from Google (or other providers) + /// Called when the app is opened with betterfit://auth/callback URL + private func handleOAuthCallback(url: URL) async { + do { + // Extract the session from the callback URL + try await supabaseClient.auth.session(from: url) + + // Session is now stored, update our state + let session = try await supabaseClient.auth.session + self.user = session.user + self.isAuthenticated = true + self.isGuest = false + } catch { + print("Failed to handle OAuth callback: \(error)") + } + } + + // MARK: - Email & Password + + /// Sign up with email and password + /// - Parameters: + /// - email: User's email address + /// - password: User's password (minimum 6 characters recommended) + @discardableResult + public func signUpWithEmail(email: String, password: String) async throws -> User { + let session = try await supabaseClient.auth.signUp( + email: email, + password: password + ) + + self.user = session.user + self.isAuthenticated = true + self.isGuest = false + + return session.user + } + + /// Sign in with email and password + /// - Parameters: + /// - email: User's email address + /// - password: User's password + @discardableResult + public func signInWithEmail(email: String, password: String) async throws -> User { + let session = try await supabaseClient.auth.signIn( + email: email, + password: password + ) + + self.user = session.user + self.isAuthenticated = true + self.isGuest = false + + return session.user + } + + // MARK: - Sign Out + + /// Sign out current user and return to guest mode + public func signOut() async throws { + try await supabaseClient.auth.signOut() + self.user = nil + self.isAuthenticated = false + self.isGuest = true + } + + // MARK: - Supabase Client Access + + /// Get Supabase client for direct database access + /// (Used by SupabasePersistenceService) + public var client: SupabaseClient { + return supabaseClient + } +} + +// MARK: - Notification Extension + +extension Notification.Name { + static let authCallbackReceived = Notification.Name("AuthCallbackReceived") +} diff --git a/Apps/iOS/BetterFitApp/UIComponents.swift b/Apps/iOS/BetterFitApp/UIComponents.swift index d914e67..22cd62d 100644 --- a/Apps/iOS/BetterFitApp/UIComponents.swift +++ b/Apps/iOS/BetterFitApp/UIComponents.swift @@ -130,3 +130,23 @@ struct MetricPill: View { .overlay { shape.stroke(theme.cardStroke, lineWidth: 1) } } } + +struct FitnessIcon: View { + let systemImage: String + let size: Double + let color: Color + + @State private var isAnimated = false + + var body: some View { + Image(systemName: systemImage) + .font(.system(size: size, weight: .semibold)) + .foregroundStyle(color) + .scaleEffect(isAnimated ? 1.1 : 1.0) + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) { + isAnimated = true + } + } + } +} diff --git a/Sources/BetterFit/BetterFit.swift b/Sources/BetterFit/BetterFit.swift index 9961dd4..cbb856f 100644 --- a/Sources/BetterFit/BetterFit.swift +++ b/Sources/BetterFit/BetterFit.swift @@ -130,7 +130,7 @@ public class BetterFit { autoTrackingService.stopTracking() } - /// Complete a workout +/// Complete a workout /// - Parameters: /// - workout: The completed workout /// - saveToHealthKit: Whether to save the workout to Apple Health (default: true, set to false for demo data) diff --git a/Sources/BetterFit/Services/Auth/AuthService.swift b/Sources/BetterFit/Services/Auth/AuthService.swift index 03bcf32..4a83739 100644 --- a/Sources/BetterFit/Services/Auth/AuthService.swift +++ b/Sources/BetterFit/Services/Auth/AuthService.swift @@ -1,209 +1,21 @@ -import Auth import Foundation -import Supabase -/// Authentication service using Supabase -/// Manages user authentication state and Apple Sign In +/// Stub auth service for testing. Real auth lives in the iOS host app. @MainActor -public final class AuthService: ObservableObject { - @Published public private(set) var user: User? - @Published public private(set) var isAuthenticated = false - @Published public private(set) var isGuest = true +public final class AuthService { + public private(set) var user: Any? + public private(set) var isAuthenticated = false + public private(set) var isGuest = true - private let supabaseClient: SupabaseClient + public init(supabaseURL: URL, supabaseAnonKey: String) {} - // MARK: - Initialization - - public init(supabaseURL: URL, supabaseAnonKey: String) { - self.supabaseClient = SupabaseClient( - supabaseURL: supabaseURL, - supabaseKey: supabaseAnonKey - ) - - // Restore session if available - Task { - await restoreSession() - } - - // Listen for OAuth callback URLs - NotificationCenter.default.addObserver( - forName: .authCallbackReceived, - object: nil, - queue: .main - ) { [weak self] notification in - guard let self else { return } - guard let url = notification.userInfo?["url"] as? URL else { return } - - Task { - await self.handleOAuthCallback(url: url) - } - } - } - - // MARK: - Session Management - - /// Restore existing session on app launch public func restoreSession() async { - do { - let session = try await supabaseClient.auth.session - self.user = session.user - self.isAuthenticated = true - self.isGuest = false - } catch { - // No session available - user is guest - self.user = nil - self.isAuthenticated = false - self.isGuest = true - } + // Stub — no real session } - /// Listen for auth state changes - public func setupAuthStateListener() { - Task { - for await state in supabaseClient.auth.authStateChanges { - let (event, session) = state - switch event { - case .signedIn, .initialSession: - self.user = session?.user - self.isAuthenticated = session?.user != nil - self.isGuest = false - - case .signedOut: - self.user = nil - self.isAuthenticated = false - self.isGuest = true - - case .tokenRefreshed: - self.user = session?.user - self.isAuthenticated = session?.user != nil - - default: - break - } - } - } - } - - // MARK: - Guest Mode - - /// Continue as guest (no authentication) public func continueAsGuest() { - self.user = nil - self.isAuthenticated = false - self.isGuest = true - } - - // MARK: - Apple Sign In - - /// Sign in with Apple ID Token - /// - Parameter idToken: Apple ID token from AuthenticationServices - /// - Returns: User session - @discardableResult - public func signInWithApple(idToken: String, nonce: String) async throws -> User { - let session = try await supabaseClient.auth.signInWithIdToken( - credentials: .init( - provider: .apple, - idToken: idToken, - nonce: nonce - ) - ) - - self.user = session.user - self.isAuthenticated = true - self.isGuest = false - - return session.user - } - - // MARK: - Google OAuth - - /// Sign in with Google OAuth - /// Note: Requires Google OAuth provider configured in Supabase - /// See: https://supabase.com/docs/guides/auth/social-login/auth-google - /// Opens the OAuth flow in the system browser - public func signInWithGoogle() async throws { - try await supabaseClient.auth.signInWithOAuth( - provider: .google, - redirectTo: URL(string: "betterfit://auth/callback") - ) + user = nil + isAuthenticated = false + isGuest = true } - - /// Handle OAuth callback from Google (or other providers) - /// Called when the app is opened with betterfit://auth/callback URL - private func handleOAuthCallback(url: URL) async { - do { - // Extract the session from the callback URL - try await supabaseClient.auth.session(from: url) - - // Session is now stored, update our state - let session = try await supabaseClient.auth.session - self.user = session.user - self.isAuthenticated = true - self.isGuest = false - } catch { - print("Failed to handle OAuth callback: \(error)") - } - } - - // MARK: - Email & Password - - /// Sign up with email and password - /// - Parameters: - /// - email: User's email address - /// - password: User's password (minimum 6 characters recommended) - @discardableResult - public func signUpWithEmail(email: String, password: String) async throws -> User { - let session = try await supabaseClient.auth.signUp( - email: email, - password: password - ) - - self.user = session.user - self.isAuthenticated = true - self.isGuest = false - - return session.user - } - - /// Sign in with email and password - /// - Parameters: - /// - email: User's email address - /// - password: User's password - @discardableResult - public func signInWithEmail(email: String, password: String) async throws -> User { - let session = try await supabaseClient.auth.signIn( - email: email, - password: password - ) - - self.user = session.user - self.isAuthenticated = true - self.isGuest = false - - return session.user - } - - // MARK: - Sign Out - - /// Sign out current user and return to guest mode - public func signOut() async throws { - try await supabaseClient.auth.signOut() - self.user = nil - self.isAuthenticated = false - self.isGuest = true - } - - // MARK: - Supabase Client Access - - /// Get Supabase client for direct database access - /// (Used by SupabasePersistenceService) - public var client: SupabaseClient { - return supabaseClient - } -} - -// MARK: - Notification Extension - -extension Notification.Name { - static let authCallbackReceived = Notification.Name("AuthCallbackReceived") } From abc88cbee0febb1ab5477167d9a33a692d9b7cdf Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Mon, 18 May 2026 23:30:01 +1000 Subject: [PATCH 2/2] fix: AppConfigurationTests cString optional unwrap for Linux CI --- Tests/BetterFitTests/AppConfigurationTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/BetterFitTests/AppConfigurationTests.swift b/Tests/BetterFitTests/AppConfigurationTests.swift index 41017d8..a25c10b 100644 --- a/Tests/BetterFitTests/AppConfigurationTests.swift +++ b/Tests/BetterFitTests/AppConfigurationTests.swift @@ -8,12 +8,12 @@ final class AppConfigurationTests: XCTestCase { /// Temporarily set an environment variable for testing private func setEnvVar(_ key: String, _ value: String) { - setenv(key.cString(using: .utf8), value.cString(using: .utf8), 1) + setenv(key.cString(using: .utf8)!, value.cString(using: .utf8)!, 1) } /// Temporarily unset an environment variable for testing private func unsetEnvVar(_ key: String) { - unsetenv(key.cString(using: .utf8)) + unsetenv(key.cString(using: .utf8)!) } override func setUp() {