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)
}
}

View File

@@ -0,0 +1,172 @@
# Mobile — SwiftUI
## Etapa 01 - Contexto e Objetivos
O objetivo é criar uma base mínima para um aplicativo de pedidos com autenticação via login, cadastro e código OTP.
----
## Etapa 02 — Esqueleto do Projeto e Views
- Criar projeto SwiftUI.
- Criar NavigationStack e views LoginView, RegistrationView e OtpView.
- Navegar entre telas.
----
## Etapa 03 — LoginView
- Logo e título "Boas-vindas!".
- Descrição "Entre para continuar seu pedido.".
- Inputs para e-mail e telefone.
- Botão "Receber Código".
- Link "Crie sua conta".
----
## Etapa 04 — RegistrationView
- Título "Crie sua conta".
- Descrição "Preencha os dados abaixo...".
- Inputs nome, e-mail e telefone.
- Termos com switch.
- Botão "Cadastrar e Receber Código".
- Link "Já tem uma conta? Entrar".
----
## Etapa 05 — OtpView
- Título "OTP".
- Texto "Enviado para <email>".
- Inputs para código OTP.
- Botão para enviar código.
- Ação "Reenviar código".
----
## Etapa 06 — View Models
- Criar view models para cada view.
- Validar campos básicos.
- Gerenciar estados de loading e erros.
----
## Etapa 07 — Validações e Máscaras
- Validar e-mail válido.
- Máscara para telefone.
- Limitar número de dígitos do OTP.
----
## Etapa 08 — Estados e Feedbacks
- Mostrar loading no botão.
- Desabilitar botão se inválido.
- Feedback de erros inline.
----
## Etapa 09 — Acessibilidade
- Labels claros.
- Áreas de toque adequadas.
- Suporte a Dynamic Type.
----
## Etapa 10 — Dark Mode
- Cores adaptadas para modo escuro.
- Testar contrastes e legibilidade.
----
## Etapa 11 — Ajustes de UI (referencias)
- Espaçamentos consistentes.
- Tipografia alinhada ao design system.
- Botões com padding padrão.
- Estados visuais claros.
----
## Etapa 11 — Checklist de Refinamento (Autenticação)
### Itens Globais
- [ ] Espaçamentos consistentes (8 / 16 / 24) entre títulos, descrições, inputs e botões.
- [ ] Cores alinhadas ao design system (backgroundLight/dark, textPrimary, primary para links/CTAs).
- [ ] Tipografia: heading1 para títulos, body para textos e labels, botão usando componente padrão.
- [ ] Estados: carregamento, desabilitado (opacidade), erro/validação inline quando necessário.
- [ ] Acessibilidade: áreas de toque ≥ 44x44, labels claros, suporte a tamanhos dinâmicos de fonte.
### Login (LoginView)
- [ ] Respiro após logo (≥ 16pt) e título “Boasvindas!” com heading1.
- [ ] Descrição “Entre para continuar seu pedido.” com cor secundária e bom contraste no dark mode.
- [ ] Inputs “Email” e “Telefone” com `LoginField` padronizado; autocapitalization off (ok).
- [ ] (Opcional) Validação leve: email válido e telefone numérico/máscara.
- [ ] CTA “Receber Código” com padding horizontal 24 e top 816; estado desabilitado se necessário.
- [ ] Texto de ajuda centralizado com padding horizontal 40.
- [ ] Link “Crie sua conta” com espaçamento 816 do texto anterior.
### Cadastro (RegistrationView)
- [ ] Título “Crie sua conta” com heading1.
- [ ] Descrição “Preencha os dados abaixo...” com body, padding top 8 e bottom 20 (ok).
- [ ] Inputs: Nome, Email, Telefone com labels; ajustar keyboardType para email/telefone.
- [ ] Termos: frase contínua sem quebras estranhas; links “Termos de Uso” e “Política de Privacidade” em primary.
- [ ] Switch dos termos posicionado à direita (ok).
- [ ] CTA “Cadastrar e Receber Código” desabilitado até Nome/Email/Telefone preenchidos e switch ativado (ok); opacidade 0.5 quando desabilitado.
- [ ] Bloco “Já tem uma conta? Entrar” com espaçamento superior 16 (ok).
- [ ] Verificar contraste dos inputs e textos em dark mode.
### OTP (OtpView)
- [ ] Título “OTP” com heading1 e texto “Enviado para <email>”.
- [ ] Inputs para 68 dígitos (conforme definição final); foco automático no primeiro campo.
- [ ] CTA desabilitado até todos os dígitos preenchidos.
- [ ] Ação “Reenviar código” com timeout/contador.
- [ ] Estados de erro (código inválido/expirado) e mensagens claras.
### Decisões em Aberto
- [ ] Termos/Privacidade: navegação interna (views com conteúdo) ou URLs externas (Link/SFSafariView)?
- [ ] Validações: mínima (campos preenchidos) ou reforçada (formato + mensagens inline)?
- [ ] Máscara de telefone: aplicar padrão BR (ex.: (11) 988887777)?
- [ ] Dark mode: paleta de cinzas/texto secundário para contraste ideal.
## Progresso — Etapa 11 (Autenticação)
Data de atualização: 2026-02-05
### Itens Globais
- [x] Espaçamentos consistentes básicos nas telas de Login e Cadastro.
- [x] Cores e tipografia aplicadas via AppColors/AppTypography.
- [ ] Revisão completa de dark mode (contraste de textos secundários).
- [ ] Estados de carregamento e mensagens de erro padronizados.
### Login (LoginView)
- [x] Estrutura com logo, título e descrição.
- [x] Inputs padronizados (LoginField) para email e telefone.
- [ ] Validação leve (formato de email/telefone) e mensagens inline.
- [ ] Estado desabilitado do CTA quando inputs vazios (opcional nesta etapa).
### Cadastro (RegistrationView)
- [x] Título e descrição com espaçamentos ajustados.
- [x] Inputs com labels e `LoginField`.
- [x] Termos com texto contínuo; links para “Termos de Uso” e “Política de Privacidade”.
- [x] Switch dos termos à direita.
- [x] CTA desabilitado até Nome/Email/Telefone preenchidos e switch ativado.
- [x] Espaçamento superior 16 no bloco “Já tem uma conta? Entrar”.
- [ ] Ajustar keyboardType para email/telefone e (opcional) máscara BR.
### OTP (OtpView)
- [x] Estrutura básica com título e indicação de email.
- [ ] Campos de dígitos (68) com foco e validação.
- [ ] CTA desabilitado até preenchimento completo.
- [ ] Reenvio de código com contador.
### Decisões em Aberto
- [ ] Termos/Privacidade: interno vs. URL externa.
- [ ] Validações: mínima vs. reforçada (com mensagens inline).
- [ ] Máscara de telefone: padrão BR.
- [ ] Dark mode: paleta de cinzas/texto secundário.

View File

@@ -0,0 +1,135 @@
# Mobile — SwiftUI
## Etapa 01 - Contexto e Objetivos
O objetivo é criar uma base mínima para um aplicativo de pedidos com autenticação via login, cadastro e código OTP.
----
## Etapa 02 — Esqueleto do Projeto e Views
- Criar projeto SwiftUI.
- Criar NavigationStack e views LoginView, RegistrationView e OtpView.
- Navegar entre telas.
----
## Etapa 03 — LoginView
- Logo e título "Boas-vindas!".
- Descrição "Entre para continuar seu pedido.".
- Inputs para e-mail e telefone.
- Botão "Receber Código".
- Link "Crie sua conta".
----
## Etapa 04 — RegistrationView
- Título "Crie sua conta".
- Descrição "Preencha os dados abaixo...".
- Inputs nome, e-mail e telefone.
- Termos com switch.
- Botão "Cadastrar e Receber Código".
- Link "Já tem uma conta? Entrar".
----
## Etapa 05 — OtpView
- Título "OTP".
- Texto "Enviado para <email>".
- Inputs para código OTP.
- Botão para enviar código.
- Ação "Reenviar código".
----
## Etapa 06 — View Models
- Criar view models para cada view.
- Validar campos básicos.
- Gerenciar estados de loading e erros.
----
## Etapa 07 — Validações e Máscaras
- Validar e-mail válido.
- Máscara para telefone.
- Limitar número de dígitos do OTP.
----
## Etapa 08 — Estados e Feedbacks
- Mostrar loading no botão.
- Desabilitar botão se inválido.
- Feedback de erros inline.
----
## Etapa 09 — Acessibilidade
- Labels claros.
- Áreas de toque adequadas.
- Suporte a Dynamic Type.
----
## Etapa 10 — Dark Mode
- Cores adaptadas para modo escuro.
- Testar contrastes e legibilidade.
----
## Etapa 11 — Ajustes de UI (referencias)
- Espaçamentos consistentes.
- Tipografia alinhada ao design system.
- Botões com padding padrão.
- Estados visuais claros.
----
## Etapa 11 — Checklist de Refinamento (Autenticação)
### Itens Globais
- [ ] Espaçamentos consistentes (8 / 16 / 24) entre títulos, descrições, inputs e botões.
- [ ] Cores alinhadas ao design system (backgroundLight/dark, textPrimary, primary para links/CTAs).
- [ ] Tipografia: heading1 para títulos, body para textos e labels, botão usando componente padrão.
- [ ] Estados: carregamento, desabilitado (opacidade), erro/validação inline quando necessário.
- [ ] Acessibilidade: áreas de toque ≥ 44x44, labels claros, suporte a tamanhos dinâmicos de fonte.
### Login (LoginView)
- [ ] Respiro após logo (≥ 16pt) e título “Boasvindas!” com heading1.
- [ ] Descrição “Entre para continuar seu pedido.” com cor secundária e bom contraste no dark mode.
- [ ] Inputs “Email” e “Telefone” com `LoginField` padronizado; autocapitalization off (ok).
- [ ] (Opcional) Validação leve: email válido e telefone numérico/máscara.
- [ ] CTA “Receber Código” com padding horizontal 24 e top 816; estado desabilitado se necessário.
- [ ] Texto de ajuda centralizado com padding horizontal 40.
- [ ] Link “Crie sua conta” com espaçamento 816 do texto anterior.
### Cadastro (RegistrationView)
- [ ] Título “Crie sua conta” com heading1.
- [ ] Descrição “Preencha os dados abaixo...” com body, padding top 8 e bottom 20 (ok).
- [ ] Inputs: Nome, Email, Telefone com labels; ajustar keyboardType para email/telefone.
- [ ] Termos: frase contínua sem quebras estranhas; links “Termos de Uso” e “Política de Privacidade” em primary.
- [ ] Switch dos termos posicionado à direita (ok).
- [ ] CTA “Cadastrar e Receber Código” desabilitado até Nome/Email/Telefone preenchidos e switch ativado (ok); opacidade 0.5 quando desabilitado.
- [ ] Bloco “Já tem uma conta? Entrar” com espaçamento superior 16 (ok).
- [ ] Verificar contraste dos inputs e textos em dark mode.
### OTP (OtpView)
- [ ] Título “OTP” com heading1 e texto “Enviado para <email>”.
- [ ] Inputs para 68 dígitos (conforme definição final); foco automático no primeiro campo.
- [ ] CTA desabilitado até todos os dígitos preenchidos.
- [ ] Ação “Reenviar código” com timeout/contador.
- [ ] Estados de erro (código inválido/expirado) e mensagens claras.
### Decisões em Aberto
- [ ] Termos/Privacidade: navegação interna (views com conteúdo) ou URLs externas (Link/SFSafariView)?
- [ ] Validações: mínima (campos preenchidos) ou reforçada (formato + mensagens inline)?
- [ ] Máscara de telefone: aplicar padrão BR (ex.: (11) 988887777)?
- [ ] Dark mode: paleta de cinzas/texto secundário para contraste ideal.

View File

@@ -4,9 +4,12 @@ import UIKit
#endif
struct HomeView: View {
@Binding var appState: AppState
@State var searchText = ""
@State var selectedCategory = "Stores"
@State var scrollOffset: CGFloat = 0
@State var hasRequestedLocation = false
let locationService = LocationService()
private let categories: [CategoryModel] = [
.init(title: "Stores", systemIcon: "storefront"),
@@ -104,6 +107,15 @@ struct HomeView: View {
.background(AppColors.backgroundLight)
.ignoresSafeArea(edges: .top)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.onAppear {
if hasRequestedLocation == false {
hasRequestedLocation = true
locationService.requestLocation { lat, lng in
appState.address.latitude = lat
appState.address.longitude = lng
}
}
}
}
private var contentStack: some View {
@@ -175,7 +187,7 @@ struct HomeView: View {
.foregroundStyle(AppColors.brandSoft)
HStack(spacing: 6) {
Text("Canggu, Kuta Utara, Bali")
Text(appState.address.display)
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textInverse)
Image(systemName: "chevron.down")

View File

@@ -4,11 +4,12 @@ struct MainTabView: View {
@Binding var selectedTab: MainTab
@Binding var root: RootFlow
let tokenStore: TokenStore
@Binding var appState: AppState
var body: some View {
TabView(selection: $selectedTab) {
NavigationStack {
HomeView()
HomeView(appState: $appState)
}
.tabItem { Label("Home", systemImage: "house.fill") }
.tag(MainTab.home)
@@ -20,7 +21,7 @@ struct MainTabView: View {
.tag(MainTab.cart)
NavigationStack {
ProfileView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
ProfileView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState)
}
.tabItem { Label("Perfil", systemImage: "person.fill") }
.tag(MainTab.profile)

View File

@@ -4,6 +4,7 @@ struct ProfileView: View {
@Binding var root: RootFlow
@Binding var selectedTab: MainTab
let tokenStore: TokenStore
@Binding var appState: AppState
var body: some View {
VStack(spacing: 20) {
@@ -29,6 +30,8 @@ struct ProfileView: View {
SecondaryButton(title: "Sair") {
tokenStore.clear()
appState.session.isAuthenticated = false
appState.session.jwt = nil
root = .auth
}
.padding(.horizontal, 20)
@@ -49,11 +52,11 @@ struct ProfileView: View {
.foregroundStyle(AppColors.textInverse)
)
Text("Daniel Loverde")
Text(appState.profile.name.isEmpty ? "Daniel Loverde" : appState.profile.name)
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
Text("daniel@pedifoods.com.br")
Text(appState.profile.email.isEmpty ? "daniel@pedifoods.com.br" : appState.profile.email)
.font(.caption)
.foregroundStyle(AppColors.secondary)
}