Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SWFrontUI/ShopwiseFrontEndUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "ShopWise uses your location to show nearby grocery stores.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -428,6 +429,7 @@
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "ShopWise uses your location to show nearby grocery stores.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down
Binary file not shown.
41 changes: 41 additions & 0 deletions SWFrontUI/ShopwiseFrontEndUI/AppToolbar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI

struct AppToolbar: ViewModifier {
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
HStack(spacing: 14) {
NavigationLink {
SettingsView()
} label: {
Image(systemName: "gearshape")
}

NavigationLink {
ProfileView()
} label: {
Image(systemName: "person.crop.circle")
}
}
}
}
}
}

extension View {
func appToolbar() -> some View {
self.modifier(AppToolbar())
}
}

extension View {
@ViewBuilder
func when(_ condition: Bool, transform: (Self) -> some View) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
37 changes: 34 additions & 3 deletions SWFrontUI/ShopwiseFrontEndUI/AuthGateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,48 @@ import SwiftUI
struct AuthGateView: View {
@EnvironmentObject var auth: AuthManager

@State private var isCheckingPreferences = false
@State private var hasCompletedOnboarding = false

var body: some View {
Group {
if auth.isSignedIn {
if !auth.isSignedIn {
AuthView()
} else if isCheckingPreferences {
ProgressView("Loading...")
} else if hasCompletedOnboarding {
ContentView()
} else {
AuthView()
NavigationStack {
OnboardingSurveyView {
hasCompletedOnboarding = true
}
}
}
}
.task {
// Restore session if tokens exist
await auth.restoreSession()
}
.task(id: auth.isSignedIn) {
await checkOnboardingStatus()
}
}

private func checkOnboardingStatus() async {
guard auth.isSignedIn else {
hasCompletedOnboarding = false
isCheckingPreferences = false
return
}

isCheckingPreferences = true
defer { isCheckingPreferences = false }

do {
let prefs = try await auth.fetchUserPreferences()
hasCompletedOnboarding = (prefs != nil)
} catch {
hasCompletedOnboarding = false
}
}
}
96 changes: 95 additions & 1 deletion SWFrontUI/ShopwiseFrontEndUI/AuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ final class AuthManager: ObservableObject {
@Published var errorMessage: String? = nil
@Published var isSignedIn: Bool = false
@Published var userEmail: String? = nil
@Published var userID: String? = nil

// Tokens
private(set) var accessToken: String? = nil
Expand Down Expand Up @@ -114,6 +115,7 @@ final class AuthManager: ObservableObject {
let session = try JSONDecoder().decode(SupabaseSession.self, from: data)
self.applySession(session)
self.userEmail = email
self.userID = session.user?.id
}
}

Expand Down Expand Up @@ -148,6 +150,7 @@ final class AuthManager: ObservableObject {
!session.access_token.isEmpty {
self.applySession(session)
self.userEmail = email
self.userID = session.user?.id
} else {
self.userEmail = email
throw AuthError.emailConfirmationRequired
Expand Down Expand Up @@ -198,11 +201,13 @@ final class AuthManager: ObservableObject {
let user = try JSONDecoder().decode(SupabaseUser.self, from: data)
self.userEmail = user.email
self.isSignedIn = true
self.userID = user.id
} catch {
// token might be expired -> try refresh
if let refreshed = try? await refreshSession() {
self.userEmail = refreshed.user?.email
self.isSignedIn = true
self.userID = refreshed.user?.id
} else {
clearSession()
}
Expand Down Expand Up @@ -250,6 +255,7 @@ final class AuthManager: ObservableObject {
private func applySession(_ session: SupabaseSession) {
self.accessToken = session.access_token
self.refreshToken = session.refresh_token
self.userID = session.user?.id
saveTokensToStorage()
self.isSignedIn = true
}
Expand All @@ -258,6 +264,7 @@ final class AuthManager: ObservableObject {
self.accessToken = nil
self.refreshToken = nil
self.userEmail = nil
self.userID = nil
self.isSignedIn = false
deleteTokensFromStorage()
}
Expand All @@ -266,7 +273,8 @@ final class AuthManager: ObservableObject {
url: URL,
method: String,
jsonBody: T?,
bearerToken: String?
bearerToken: String?,
extraHeaders: [String: String] = [:]
) async throws -> (Data, HTTPURLResponse) {

var req = URLRequest(url: url)
Expand All @@ -278,6 +286,10 @@ final class AuthManager: ObservableObject {
req.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
}

for (key, value) in extraHeaders {
req.setValue(value, forHTTPHeaderField: key)
}

if let jsonBody {
req.httpBody = try JSONEncoder().encode(jsonBody)
}
Expand Down Expand Up @@ -412,3 +424,85 @@ extension AuthManager {
return try JSONDecoder().decode([WalmartItem].self, from: data)
}
}

//Onboard survey fetch/save
struct UserPreferencesRow: Codable {
let user_id: String
let diet_preferences: [String]
let allergies: [String]
}

struct UserPreferencesUpsertBody: Codable {
let user_id: String
let diet_preferences: [String]
let allergies: [String]
}

extension AuthManager {
func fetchUserPreferences() async throws -> UserPreferences? {
guard let token = accessToken, let uid = userID else {
throw URLError(.userAuthenticationRequired)
}

let base = supabaseURL
.appendingPathComponent("rest/v1")
.appendingPathComponent("user_preferences")

var comps = URLComponents(url: base, resolvingAgainstBaseURL: false)!
comps.queryItems = [
URLQueryItem(name: "select", value: "user_id,diet_preferences,allergies"),
URLQueryItem(name: "user_id", value: "eq.\(uid)"),
URLQueryItem(name: "limit", value: "1")
]

guard let url = comps.url else { throw URLError(.badURL) }

let (data, _) = try await request(
url: url,
method: "GET",
jsonBody: Optional<String>.none,
bearerToken: token
)

let rows = try JSONDecoder().decode([UserPreferencesRow].self, from: data)
guard let row = rows.first else { return nil }

return UserPreferences(
dietPreferences: row.diet_preferences,
allergies: row.allergies
)
}

func saveUserPreferences(_ preferences: UserPreferences) async throws {
guard let token = accessToken, let uid = userID else {
throw URLError(.userAuthenticationRequired)
}

let base = supabaseURL
.appendingPathComponent("rest/v1")
.appendingPathComponent("user_preferences")

var comps = URLComponents(url: base, resolvingAgainstBaseURL: false)!
comps.queryItems = [
URLQueryItem(name: "on_conflict", value: "user_id")
]

guard let url = comps.url else { throw URLError(.badURL) }

let body = UserPreferencesUpsertBody(
user_id: uid,
diet_preferences: preferences.dietPreferences,
allergies: preferences.allergies
)

_ = try await request(
url: url,
method: "POST",
jsonBody: body,
bearerToken: token,
extraHeaders: [
"Prefer": "resolution=merge-duplicates,return=representation"
]
)
}
}
27 changes: 21 additions & 6 deletions SWFrontUI/ShopwiseFrontEndUI/CartStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import Combine

// Simple cart line item model (separate from your SwiftData Item model)
struct CartLineItem: Identifiable, Hashable {
let id: UUID
let id: String
let name: String
let unit: String
let price: Double
var quantity: Int

init(id: UUID = UUID(), name: String, unit: String, price: Double, quantity: Int = 1) {
init(id: String = String(), name: String, unit: String, price: Double, quantity: Int = 1) {
self.id = id
self.name = name
self.unit = unit
Expand All @@ -23,7 +23,6 @@ struct CartLineItem: Identifiable, Hashable {
final class CartStore: ObservableObject {
@Published private(set) var items: [CartLineItem] = []

//amount of cart items
var itemCount: Int {
items.reduce(0) { $0 + $1.quantity }
}
Expand All @@ -32,11 +31,27 @@ final class CartStore: ObservableObject {
items.reduce(0) { $0 + $1.lineTotal }
}

func add(name: String, unit: String, price: Double) {
if let idx = items.firstIndex(where: { $0.name == name && $0.unit == unit && $0.price == price }) {
func add(product: Product) {
add(id: product.id, name: product.name, unit: product.unit, price: product.price)
}

func add(ingredient: Ingredient) {
add(id: ingredient.id, name: ingredient.name, unit: ingredient.unit, price: ingredient.price)
}

func add(id: String, name: String, unit: String, price: Double) {
if let idx = items.firstIndex(where: { $0.id == id }) {
items[idx].quantity += 1
} else {
items.append(CartLineItem(name: name, unit: unit, price: price))
items.append(
CartLineItem(
id: id,
name: name,
unit: unit,
price: price,
quantity: 1
)
)
}
}

Expand Down
Loading
Loading