Login flow fixed
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
172
pedi-foods/Sources/PediFoods/Views/Auth/pedifoods_app_plan 2.md
Normal file
172
pedi-foods/Sources/PediFoods/Views/Auth/pedifoods_app_plan 2.md
Normal 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 “Boas‑vindas!” com heading1.
|
||||
- [ ] Descrição “Entre para continuar seu pedido.” com cor secundária e bom contraste no dark mode.
|
||||
- [ ] Inputs “E‑mail” e “Telefone” com `LoginField` padronizado; autocapitalization off (ok).
|
||||
- [ ] (Opcional) Validação leve: e‑mail válido e telefone numérico/máscara.
|
||||
- [ ] CTA “Receber Código” com padding horizontal 24 e top 8–16; estado desabilitado se necessário.
|
||||
- [ ] Texto de ajuda centralizado com padding horizontal 40.
|
||||
- [ ] Link “Crie sua conta” com espaçamento 8–16 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, E‑mail, Telefone com labels; ajustar keyboardType para e‑mail/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/E‑mail/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 6–8 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) 98888‑7777)?
|
||||
- [ ] 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 e‑mail e telefone.
|
||||
- [ ] Validação leve (formato de e‑mail/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/E‑mail/Telefone preenchidos e switch ativado.
|
||||
- [x] Espaçamento superior 16 no bloco “Já tem uma conta? Entrar”.
|
||||
- [ ] Ajustar keyboardType para e‑mail/telefone e (opcional) máscara BR.
|
||||
|
||||
### OTP (OtpView)
|
||||
- [x] Estrutura básica com título e indicação de e‑mail.
|
||||
- [ ] Campos de dígitos (6–8) 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.
|
||||
135
pedi-foods/Sources/PediFoods/Views/Auth/pedifoods_app_plan.md
Normal file
135
pedi-foods/Sources/PediFoods/Views/Auth/pedifoods_app_plan.md
Normal 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 “Boas‑vindas!” com heading1.
|
||||
- [ ] Descrição “Entre para continuar seu pedido.” com cor secundária e bom contraste no dark mode.
|
||||
- [ ] Inputs “E‑mail” e “Telefone” com `LoginField` padronizado; autocapitalization off (ok).
|
||||
- [ ] (Opcional) Validação leve: e‑mail válido e telefone numérico/máscara.
|
||||
- [ ] CTA “Receber Código” com padding horizontal 24 e top 8–16; estado desabilitado se necessário.
|
||||
- [ ] Texto de ajuda centralizado com padding horizontal 40.
|
||||
- [ ] Link “Crie sua conta” com espaçamento 8–16 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, E‑mail, Telefone com labels; ajustar keyboardType para e‑mail/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/E‑mail/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 6–8 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) 98888‑7777)?
|
||||
- [ ] Dark mode: paleta de cinzas/texto secundário para contraste ideal.
|
||||
Reference in New Issue
Block a user