- SwiftUI + async/await architecture - Barcode scanning with AVFoundation - Product display with score ring, NOVA badge, nutrition - Alternatives with sort/filter - Auth (login/register) - Favorites & history - Account management - Dark theme - Connected to food.payfrit.com API (Open Food Facts proxy) Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
4.9 KiB
Swift
140 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
struct RegisterSheet: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var name = ""
|
|
@State private var email = ""
|
|
@State private var password = ""
|
|
@State private var zipCode = ""
|
|
@State private var isLoading = false
|
|
@State private var error: String?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
// Logo
|
|
Image(systemName: "leaf.circle.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.green)
|
|
.padding(.top, 40)
|
|
|
|
Text("Create Account")
|
|
.font(.title.bold())
|
|
|
|
Text("Sign up to save favorites, track your scan history, and get personalized recommendations.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
|
|
// Form
|
|
VStack(spacing: 16) {
|
|
TextField("Name", text: $name)
|
|
.textContentType(.name)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
TextField("Email", text: $email)
|
|
.keyboardType(.emailAddress)
|
|
.textContentType(.emailAddress)
|
|
.autocapitalization(.none)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
SecureField("Password", text: $password)
|
|
.textContentType(.newPassword)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
TextField("ZIP Code", text: $zipCode)
|
|
.keyboardType(.numberPad)
|
|
.textContentType(.postalCode)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
Text("We use your ZIP code to find stores near you.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
if let error = error {
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
|
|
// Register Button
|
|
Button {
|
|
register()
|
|
} label: {
|
|
if isLoading {
|
|
ProgressView()
|
|
.tint(.black)
|
|
} else {
|
|
Text("Create Account")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
.foregroundColor(.black)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(isValid ? Color.green : Color.gray)
|
|
.cornerRadius(12)
|
|
.disabled(!isValid || isLoading)
|
|
.padding(.horizontal, 24)
|
|
|
|
// Login Link
|
|
HStack {
|
|
Text("Already have an account?")
|
|
.foregroundColor(.secondary)
|
|
Button("Log In") {
|
|
dismiss()
|
|
appState.showLoginSheet = true
|
|
}
|
|
.foregroundColor(.green)
|
|
}
|
|
.font(.subheadline)
|
|
|
|
Spacer(minLength: 40)
|
|
}
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Cancel") {
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isValid: Bool {
|
|
!name.isEmpty && !email.isEmpty && email.contains("@") && password.count >= 6 && zipCode.count >= 5
|
|
}
|
|
|
|
private func register() {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
Task {
|
|
do {
|
|
let (profile, token) = try await APIService.shared.register(
|
|
email: email,
|
|
password: password,
|
|
name: name,
|
|
zipCode: zipCode
|
|
)
|
|
await appState.setAuth(userId: profile.id, token: token, profile: profile)
|
|
await MainActor.run {
|
|
dismiss()
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error.localizedDescription
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|