import SwiftUI enum Route: Hashable { case terms, policy case registration, loginEmail } struct AuthFlowView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @State var path: [Route] = [] var body: some View { NavigationStack(path: $path) { LoginView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState) { route in path.append(route) } .navigationDestination(for: Route.self) { route in switch route { case .terms: TermsOfUseView() case .policy: PrivacyPolicyView() case .registration: RegistrationView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, navigate: { route in path.append(route) }) case .loginEmail: LoginEmailView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState) } } } } } struct LoginView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @Environment(\.colorScheme) var colorScheme let navigate: (Route) -> Void @ViewBuilder private var logoImage: some View { #if os(Android) Image(colorScheme == .dark ? "pedifoods_dark" : "pedifoods", bundle: .module) .resizable() #else SwiftUI.Image("pedifoods") .resizable() #endif } @ViewBuilder private var pinHeroImage: some View { #if os(Android) Image(colorScheme == .dark ? "pin_image_app_dark" : "pin_image_app", bundle: .module) .resizable() #else SwiftUI.Image("pin_image_app") .resizable() #endif } var body: some View { GeometryReader { geo in let heroHeight = max(360, geo.size.height * 0.44) ZStack(alignment: .top) { (colorScheme == .dark ? Color.black : AppColors.backgroundLight) .ignoresSafeArea() pinHeroImage .scaledToFill() .frame(height: heroHeight + 80) .offset(y: -60) .mask( LinearGradient( colors: [.black, .black, .black.opacity(0.0)], startPoint: .top, endPoint: .bottom ) ) VStack(spacing: 18) { Spacer().frame(height: heroHeight - 100) logoImage .scaledToFit() .frame(height: 180) Text("Descubra lorem ipsum ba bla") .font(AppTypography.heading1) .foregroundStyle(colorScheme == .dark ? AppColors.textInverse : AppColors.textPrimary) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 28) Spacer().frame(height: 40) Button { navigate(.loginEmail) } label: { PrimaryButtonLabel(title: "ENTRAR") } .padding(.horizontal, 28) .tint(AppColors.tertiary) HStack(spacing: 6) { Text("Não tem conta ainda?") .foregroundStyle(colorScheme == .dark ? AppColors.textInverse.opacity(0.9) : AppColors.textPrimary) Button("Criar conta") { navigate(.registration) } .foregroundStyle(AppColors.primary) } .font(AppTypography.body) Spacer().frame(height: 12) } } .ignoresSafeArea() } } } struct RegistrationView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @State var name = "" @State var email = "" @State var phone = "" @State var acceptedTerms = false @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme let navigate: (Route) -> Void private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms } @ViewBuilder private var logoImage: some View { #if os(Android) Image(colorScheme == .dark ? "pedifoods_dark" : "pedifoods", bundle: .module) .resizable() #else SwiftUI.Image("pedifoods") .resizable() #endif } var body: some View { ZStack { (colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea() ScrollView { VStack(spacing: 0) { logoImage .scaledToFit() .frame(width: 120, height: 120) Text("Crie sua conta") .font(AppTypography.heading1) .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) Text("Preencha os dados abaixo para começar.") .font(AppTypography.body) .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .padding(.top, 8) .padding(.bottom, 20) VStack(spacing: 16) { VStack(alignment: .leading, spacing: 16) { LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name) LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email) LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone) } } .padding(.horizontal, 24) HStack(alignment: .top, spacing: 12) { Toggle("", isOn: $acceptedTerms) .labelsHidden() .toggleStyle(SwitchToggleStyle(tint: AppColors.primary)) Group { Text("Ao se cadastrar, você concorda com nossos [Termos de Uso](app://terms) e [Política de Privacidade](app://policy)") .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .tint(AppColors.primary) .environment(\.openURL, OpenURLAction { url in guard url.scheme == "app" else { return .handled } switch url.host { case "terms": navigate(.terms) return .handled case "policy": navigate(.policy) return .handled default: return .handled } }) } .multilineTextAlignment(.leading) } .padding(.horizontal, 24) .padding(.top, 16) .padding(.bottom, 16) PrimaryButton(title: "Cadastrar e Receber Código", image: Image(systemName: "arrow.right")) { // TODO: integrar com ApiService e OTP tokenStore.jwt = "demo-token" appState.session.isAuthenticated = true appState.session.jwt = "demo-token" appState.profile.email = email selectedTab = .home root = .main } .padding(.horizontal, 24) .padding(.top, 6) .disabled(!isFormValid) .opacity(isFormValid ? 1.0 : 0.5) .tint(AppColors.tertiary) HStack(spacing: 6) { Text("Já tem uma conta?") .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.8) : AppColors.secondary) NavigationLink("Entrar") { LoginEmailView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState) } .foregroundStyle(AppColors.primary) } .font(AppTypography.body) .padding(.top, 16) Spacer().frame(height: 12) } } .padding(.top, -40) } } } struct LoginEmailView: View { @Binding var root: RootFlow @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState @State var email = "" @State var phone = "" @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @ViewBuilder private var logoImage: some View { #if os(Android) Image(colorScheme == .dark ? "pedifoods_dark" : "pedifoods", bundle: .module) .resizable() #else SwiftUI.Image("pedifoods") .resizable() #endif } var body: some View { ZStack { (colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea() VStack(spacing: 0) { logoImage .scaledToFit() .frame(width: 120, height: 120) Text("Boas-vindas!") .font(AppTypography.heading1) .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .padding(Edge.Set.top, 8) .padding(.bottom, 16) VStack(spacing: 16) { VStack(alignment: .leading, spacing: 16) { LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email) LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone) } } .padding(.horizontal, 24) .padding(.bottom, 16) PrimaryButton(title: "Receber Código", image: Image(systemName: "arrow.right")) { // TODO: integrar com ApiService e OTP tokenStore.jwt = "demo-token" appState.session.isAuthenticated = true appState.session.jwt = "demo-token" appState.profile.email = email selectedTab = .home root = .main } .padding(.horizontal, 24) .tint(AppColors.tertiary) Text("Enviaremos um código de verificação por SMS \nou E-mail para confirmar seu acesso.") .font(.caption) .foregroundStyle(Color.gray) .multilineTextAlignment(.center) .padding(.horizontal, 40) .padding([.top, .bottom], 16) HStack(spacing: 6) { Text("Novo por aqui?") .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.8) : Color.gray) Text("Crie sua conta") .foregroundStyle(AppColors.primary) .onTapGesture { dismiss() } } .font(AppTypography.body) .padding(.top, 8) Spacer() } } } } struct LoginField: View { let icon: String let placeholder: String @Binding var text: String var body: some View { HStack(spacing: 12) { Image(systemName: icon) .foregroundStyle(Color.gray) .frame(width: 28) TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { } .textInputAutocapitalization(.never) .autocorrectionDisabled() .foregroundColor(.black) } .padding(.horizontal, 16) .frame(height: 52) .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(Color.black.opacity(0.06), lineWidth: 1) ) } } struct OtpView: View { let email: String var body: some View { VStack(spacing: 16) { Text("OTP") .font(AppTypography.heading1) .foregroundStyle(Color.white) Text("Enviado para \(email)") .font(.caption) .foregroundStyle(Color.white.opacity(0.8)) } .padding(24) .background(AppColors.backgroundLight) .ignoresSafeArea() } } struct TermsOfUseView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Termos de Uso") .font(AppTypography.heading1) .foregroundStyle(AppColors.textPrimary) Text("Conteúdo dos termos de uso...") .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) } .padding(24) } .background((Color(UIColor { trait in trait.userInterfaceStyle == .dark ? .black : UIColor(AppColors.backgroundLight) })).ignoresSafeArea()) .navigationTitle("Termos de Uso") .navigationBarTitleDisplayMode(.inline) } } struct PrivacyPolicyView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Política de Privacidade") .font(AppTypography.heading1) .foregroundStyle(AppColors.textPrimary) Text("Conteúdo da política de privacidade...") .font(AppTypography.body) .foregroundStyle(AppColors.textPrimary) } .padding(24) } .background((Color(UIColor { trait in trait.userInterfaceStyle == .dark ? .black : UIColor(AppColors.backgroundLight) })).ignoresSafeArea()) .navigationTitle("Privacidade") .navigationBarTitleDisplayMode(.inline) } }