Login flow fixed

This commit is contained in:
Daniel Arantes Loverde
2026-02-05 16:57:27 -03:00
parent 01c1da0430
commit 51d02f6715
12 changed files with 859 additions and 108 deletions

View File

@@ -1,13 +1,35 @@
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 {
LoginView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
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)
}
}
}
}
}
@@ -16,9 +38,10 @@ struct LoginView: View {
@Binding var root: RootFlow
@Binding var selectedTab: MainTab
let tokenStore: TokenStore
@State var email = ""
@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)
@@ -40,59 +63,64 @@ struct LoginView: View {
}
var body: some View {
ZStack(alignment: .top) {
(colorScheme == .dark ? AppColors.backgroundDark : AppColors.backgroundLight)
.ignoresSafeArea()
GeometryReader { geo in
let heroHeight = max(360, geo.size.height * 0.44)
pinHeroImage
.scaledToFill()
.frame(height: 520)
.offset(y: -90)
.mask(
LinearGradient(
colors: [.black, .black, .black.opacity(0.0)],
startPoint: .top,
endPoint: .bottom
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: 20) {
Spacer().frame(height: 280)
logoImage
.scaledToFit()
.frame(height: 200)
VStack(spacing: 18) {
Spacer().frame(height: heroHeight - 100)
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()
PrimaryButton(title: "ENTRAR") {
// TODO: integrar com ApiService e OTP
tokenStore.jwt = "demo-token"
selectedTab = .home
root = .main
}
.padding(.horizontal, 28)
logoImage
.scaledToFit()
.frame(height: 180)
HStack(spacing: 6) {
Text("Não tem conta ainda?")
.foregroundStyle(colorScheme == .dark ? AppColors.textInverse.opacity(0.9) : AppColors.textPrimary)
NavigationLink("Criar conta") {
RegistrationView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
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")
}
.foregroundStyle(AppColors.primary)
}
.font(AppTypography.body)
.padding(.horizontal, 28)
.tint(AppColors.tertiary)
Spacer().frame(height: 24)
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()
}
.ignoresSafeArea()
}
}
@@ -100,33 +128,228 @@ 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 {
ScrollView {
VStack(spacing: 20) {
Text("Cadastro")
.font(AppTypography.heading1)
.foregroundStyle(AppColors.textPrimary)
ZStack {
(colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()
VStack(spacing: 12) {
TextField("Nome", text: $name)
.textFieldStyle(.roundedBorder)
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
TextField("Telefone", text: $phone)
.textFieldStyle(.roundedBorder)
ScrollView {
VStack(spacing: 0) {
logoImage
.scaledToFit()
.frame(width: 120, height: 120)
PrimaryButton(title: "Enviar OTP") {
// TODO: solicitar OTP
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(24)
.padding(.top, -40)
}
.background(AppColors.backgroundLight)
}
}
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)
)
}
}
@@ -137,13 +360,52 @@ struct OtpView: View {
VStack(spacing: 16) {
Text("OTP")
.font(AppTypography.heading1)
.foregroundStyle(AppColors.textPrimary)
.foregroundStyle(Color.white)
Text("Enviado para \(email)")
.font(.caption)
.foregroundStyle(AppColors.secondary)
.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)
}
}