[auth-payment-login-lockout] Unify auth back button and animate the flow entry

- RegistrationView / LoginEmailView / OtpView: pushed with no nav bar, so
  they showed the oversized iOS 26 system glass back button. Wrapped each
  in LCENavigationView with the standard AppBackButtonIcon, matching
  LoginView and every other screen. Dropped the now-dead colorScheme dark
  branches and forced .preferredColorScheme(.light) (app has no dark
  theme).
- ContentView: root .auth <-> .main switched with no transition. Added
  .move transitions on both branches and wrapped enterAuthFlow() /
  LoginView's back action in withAnimation, so opening auth from Profile's
  'Entrar ou Cadastrar' now slides in and back slides out.
- ProfileLoggedOutFlowTests: new test asserting the pushed auth screens
  carry the LCENavigationView back button.
This commit is contained in:
Daniel Arantes Loverde
2026-08-27 10:59:06 -03:00
parent 21250ec2b9
commit 0f0672f18d
6 changed files with 81 additions and 15 deletions

View File

@@ -32,11 +32,13 @@ struct ContentView: View {
tokenStore: tokenStore, tokenStore: tokenStore,
appState: $appState appState: $appState
) )
.transition(.move(edge: .trailing))
case .main: case .main:
if isBootstrappingSession { if isBootstrappingSession {
sessionBootstrapLoadingView sessionBootstrapLoadingView
} else { } else {
MainTabView(selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, enterAuth: enterAuthFlow) MainTabView(selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, enterAuth: enterAuthFlow)
.transition(.move(edge: .leading))
} }
} }
} }
@@ -408,7 +410,9 @@ struct ContentView: View {
/// decisions/2026-08-27-payment-auth-lockout-clean-intro-screen.md. /// decisions/2026-08-27-payment-auth-lockout-clean-intro-screen.md.
@MainActor @MainActor
private func enterAuthFlow() { private func enterAuthFlow() {
root = .auth withAnimation(.easeInOut(duration: 0.3)) {
root = .auth
}
} }
@MainActor @MainActor

View File

@@ -1,4 +1,7 @@
import SwiftUI import SwiftUI
#if canImport(LCEssentials)
import LCEssentials
#endif
struct LoginEmailView: View { struct LoginEmailView: View {
@Binding var root: RootFlow @Binding var root: RootFlow
@@ -11,7 +14,6 @@ struct LoginEmailView: View {
@State var isLoading = false @State var isLoading = false
@State var errorMessage: String? @State var errorMessage: String?
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@ViewBuilder private var logoImage: some View { @ViewBuilder private var logoImage: some View {
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
@@ -19,8 +21,18 @@ struct LoginEmailView: View {
} }
var body: some View { var body: some View {
LCENavigationView {
content
}
.setLeftButton(image: AnyView(AppBackButtonIcon())) { dismiss() }
.setNavigationBarBackgroundColor(AppColors.backgroundLight)
.buttonStyle(.plain)
.preferredColorScheme(.light)
}
private var content: some View {
ZStack { ZStack {
(colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea() AppColors.backgroundLight.ignoresSafeArea()
VStack(spacing: 0) { VStack(spacing: 0) {
logoImage logoImage
@@ -29,7 +41,7 @@ struct LoginEmailView: View {
Text("Boas-vindas!") Text("Boas-vindas!")
.font(AppTypography.heading1) .font(AppTypography.heading1)
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
.padding(Edge.Set.top, 8) .padding(Edge.Set.top, 8)
.padding(.bottom, 16) .padding(.bottom, 16)
@@ -66,7 +78,7 @@ struct LoginEmailView: View {
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Novo por aqui?") Text("Novo por aqui?")
.foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.8) : Color.gray) .foregroundStyle(Color.gray)
Text("Crie sua conta") Text("Crie sua conta")
.foregroundStyle(AppColors.primary) .foregroundStyle(AppColors.primary)
.onTapGesture { .onTapGesture {

View File

@@ -16,7 +16,9 @@ struct LoginView: View {
LCENavigationView { LCENavigationView {
content content
} }
.setLeftButton(image: AnyView(AppBackButtonIcon())) { root = .main } .setLeftButton(image: AnyView(AppBackButtonIcon())) {
withAnimation(.easeInOut(duration: 0.3)) { root = .main }
}
.setTitle(text: Text("Acesse sua conta").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)) .setTitle(text: Text("Acesse sua conta").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary))
.setNavigationBarBackgroundColor(AppColors.backgroundLight) .setNavigationBarBackgroundColor(AppColors.backgroundLight)
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@@ -1,5 +1,8 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
#if canImport(LCEssentials)
import LCEssentials
#endif
struct OtpView: View { struct OtpView: View {
private let resendDelaySeconds = 45 private let resendDelaySeconds = 45
@@ -21,13 +24,23 @@ struct OtpView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
LCENavigationView {
content
}
.setLeftButton(image: AnyView(AppBackButtonIcon())) { dismiss() }
.setNavigationBarBackgroundColor(AppColors.backgroundLight)
.buttonStyle(.plain)
.preferredColorScheme(.light)
}
private var content: some View {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
SwiftUI.Image("pedifoods") SwiftUI.Image("pedifoods")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 74, height: 74) .frame(width: 74, height: 74)
.padding(.top, 140) .padding(.top, 48)
Text("Verificação") Text("Verificação")
.font(AppTypography.heading1) .font(AppTypography.heading1)
@@ -122,7 +135,6 @@ struct OtpView: View {
} }
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.background(AppColors.backgroundLight) .background(AppColors.backgroundLight)
.ignoresSafeArea()
.onAppear { .onAppear {
isOtpFocused = true isOtpFocused = true
startResendCooldown() startResendCooldown()

View File

@@ -1,4 +1,7 @@
import SwiftUI import SwiftUI
#if canImport(LCEssentials)
import LCEssentials
#endif
struct RegistrationView: View { struct RegistrationView: View {
@Binding var root: RootFlow @Binding var root: RootFlow
@@ -12,7 +15,6 @@ struct RegistrationView: View {
@State var isLoading = false @State var isLoading = false
@State var errorMessage: String? @State var errorMessage: String?
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
let navigate: (Route) -> Void let navigate: (Route) -> Void
private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms } private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms }
@@ -23,8 +25,18 @@ struct RegistrationView: View {
} }
var body: some View { var body: some View {
LCENavigationView {
content
}
.setLeftButton(image: AnyView(AppBackButtonIcon())) { dismiss() }
.setNavigationBarBackgroundColor(AppColors.backgroundLight)
.buttonStyle(.plain)
.preferredColorScheme(.light)
}
private var content: some View {
ZStack { ZStack {
(colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea() AppColors.backgroundLight.ignoresSafeArea()
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -34,11 +46,11 @@ struct RegistrationView: View {
Text("Crie sua conta") Text("Crie sua conta")
.font(AppTypography.heading1) .font(AppTypography.heading1)
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
Text("Preencha os dados abaixo para começar.") Text("Preencha os dados abaixo para começar.")
.font(AppTypography.body) .font(AppTypography.body)
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
.padding(.top, 8) .padding(.top, 8)
.padding(.bottom, 20) .padding(.bottom, 20)
@@ -64,7 +76,7 @@ struct RegistrationView: View {
Group { Group {
Text("Ao se cadastrar, você concorda com nossos [Termos de Uso](app://terms) e [Política de Privacidade](app://policy)") 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) .foregroundStyle(AppColors.textPrimary)
.tint(AppColors.primary) .tint(AppColors.primary)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
guard url.scheme == "app" else { return .handled } guard url.scheme == "app" else { return .handled }
@@ -98,7 +110,7 @@ struct RegistrationView: View {
HStack(spacing: 6) { HStack(spacing: 6) {
Text("Já tem uma conta?") Text("Já tem uma conta?")
.foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.8) : AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
NavigationLink("Entrar") { NavigationLink("Entrar") {
LoginEmailView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, navigate: navigate) LoginEmailView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, navigate: navigate)
} }
@@ -110,7 +122,6 @@ struct RegistrationView: View {
Spacer().frame(height: 12) Spacer().frame(height: 12)
} }
} }
.padding(.top, -40)
} }
} }

View File

@@ -40,4 +40,29 @@ final class ProfileLoggedOutFlowTests: XCTestCase {
// Back out of `.auth` lands on the main tab bar again. // Back out of `.auth` lands on the main tab bar again.
XCTAssertTrue(app.buttons["Home"].waitForExistence(timeout: 5), "Back button did not return to the main flow") XCTAssertTrue(app.buttons["Home"].waitForExistence(timeout: 5), "Back button did not return to the main flow")
} }
/// The pushed auth screens (Registration, LoginEmail) must carry the
/// app's standard `AppBackButtonIcon` via `LCENavigationView`, not the
/// oversized iOS 26 system glass back button.
func testPushedAuthScreensUseAppBackButton() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachLoggedOutProfile())
app.buttons["Entrar ou Cadastrar"].tap()
XCTAssertTrue(app.buttons["Criar conta"].waitForExistence(timeout: 5))
app.buttons["Criar conta"].tap()
XCTAssertTrue(app.staticTexts["Crie sua conta"].waitForExistence(timeout: 5))
let regBack = app.buttons.matching(NSPredicate(format: "label == %@", "Back")).firstMatch
XCTAssertTrue(regBack.exists, "Registration screen has no LCENavigationView back button")
regBack.tap()
XCTAssertTrue(app.buttons["Entrar"].waitForExistence(timeout: 5))
app.buttons["Entrar"].tap()
XCTAssertTrue(app.staticTexts["Boas-vindas!"].waitForExistence(timeout: 5))
let loginBack = app.buttons.matching(NSPredicate(format: "label == %@", "Back")).firstMatch
XCTAssertTrue(loginBack.exists, "Login e-mail screen has no LCENavigationView back button")
loginBack.tap()
XCTAssertTrue(app.buttons["Criar conta"].waitForExistence(timeout: 5), "Back did not return to the auth intro")
}
} }