[auth-payment-login-lockout] Make auth intro a clean screen with a back button

App Review rejected the 2026-08 build: tapping checkout as a guest
switched root to .auth, which replaced the whole UI with LoginView as the
NavigationStack root - no nav bar, no back, no dismiss. User was trapped.

- LoginView: rebuilt as a static screen in LCENavigationView with a back
  button that sets root = .main, a 'Criar conta' and an 'Entrar' button,
  and 'Termos de Uso' / 'Politica de Privacidade' links. Added #Preview.
- Forced .preferredColorScheme(.light) and fixed DS colors (the app has
  no dark theme, so system dark mode was inverting the nav bar / title).
- Removed the entry-reveal animation machinery (heroVisible/textVisible/
  buttonVisible/token/prepare flags across LoginView, AuthFlowView,
  ContentView) - dead since guest browsing shipped and the root cause of
  the 2026-08-06 'content stuck hidden' bug.
- enterAuthFlow() is now just root = .auth.
- UITestSupport.ensureLoggedIn taps 'Entrar' (new label).
- ProfileLoggedOutFlowTests: new test covering the choices and the way
  back out of .auth.
- Localizable.xcstrings: catalog caught up to the new/removed strings.
This commit is contained in:
Daniel Arantes Loverde
2026-08-27 10:11:04 -03:00
parent 662f7d6ebd
commit 57103c637a
6 changed files with 81 additions and 170 deletions

View File

@@ -11,21 +11,12 @@ struct AuthFlowView: View {
@Binding var selectedTab: MainTab
let tokenStore: TokenStore
@Binding var appState: AppState
var shouldPrepareLoginEntry: Bool = false
var authEntryAnimationToken: Int = 0
@State var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
LoginView(
root: $root,
selectedTab: $selectedTab,
tokenStore: tokenStore,
appState: $appState,
shouldPrepareEntryAnimation: shouldPrepareLoginEntry,
authEntryAnimationToken: authEntryAnimationToken
) { route in
LoginView(root: $root) { route in
path.append(route)
}
.navigationDestination(for: Route.self) { route in

View File

@@ -1,142 +1,79 @@
import SwiftUI
#if canImport(LCEssentials)
import LCEssentials
#endif
struct LoginView: View {
@Binding var root: RootFlow
@Binding var selectedTab: MainTab
let tokenStore: TokenStore
@Binding var appState: AppState
@Environment(\.colorScheme) var colorScheme
var shouldPrepareEntryAnimation: Bool = false
var authEntryAnimationToken: Int = 0
let navigate: (Route) -> Void
@State var heroVisible = true
@State var textVisible = true
@State var buttonVisible = true
@State var lastAnimatedToken = 0
@ViewBuilder private var logoImage: some View {
SwiftUI.Image("pedifoods")
.resizable()
}
@ViewBuilder private var pinHeroImage: some View {
SwiftUI.Image("pin_image_app")
.resizable()
}
var body: some View {
GeometryReader { geo in
let heroHeight = max(360, geo.size.height * 0.44)
let logoTopInset = max(0, (geo.size.height - 180) / 2)
LCENavigationView {
content
}
.setLeftButton(image: AnyView(AppBackButtonIcon())) { root = .main }
.setTitle(text: Text("Acesse sua conta").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary))
.setNavigationBarBackgroundColor(AppColors.backgroundLight)
.buttonStyle(.plain)
// The app ships no dark theme; force light so system dark mode
// can't invert the nav bar / title against the fixed DS colors.
.preferredColorScheme(.light)
}
ZStack(alignment: .top) {
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
.ignoresSafeArea()
private var content: some View {
VStack(spacing: 20) {
Spacer()
pinHeroImage
.scaledToFill()
.frame(height: heroHeight + 80)
.offset(y: heroVisible ? -60 : -(heroHeight + 220))
.mask(
LinearGradient(
colors: [.black, .black, .black.opacity(0.0)],
startPoint: .top,
endPoint: .bottom
)
)
logoImage
.scaledToFit()
.frame(height: 160)
VStack(spacing: 18) {
Spacer().frame(height: logoTopInset)
Text("Sua vontade, no seu tempo.\nTudo o que você precisa, em um toque.")
.font(AppTypography.heading25)
.foregroundStyle(AppColors.textPrimary)
.multilineTextAlignment(.center)
.padding(.horizontal, 28)
logoImage
.scaledToFit()
.frame(height: 180)
Spacer()
Text("Sua vontade, no seu tempo.\nTudo o que você precisa, em um toque.")
.font(AppTypography.heading25)
.foregroundStyle(colorScheme == .dark ? AppColors.textInverse : AppColors.textPrimary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 28)
.opacity(textVisible ? 1.0 : 0.0)
.offset(y: textVisible ? 0 : 24)
VStack(spacing: 12) {
PrimaryButton(title: "Criar conta", image: Image(systemName: "arrow.right")) {
navigate(.registration)
}
.tint(AppColors.tertiary)
Spacer().frame(height: 40)
Button {
navigate(.loginEmail)
} label: {
PrimaryButtonLabel(title: "ENTRAR")
}
.padding(.horizontal, 28)
.tint(AppColors.tertiary)
.offset(y: buttonVisible ? 0 : 140)
.opacity(buttonVisible ? 1.0 : 0.0)
.buttonStyle(.plain)
HStack(spacing: 6) {
Text("Não tem conta ainda?")
.foregroundStyle(colorScheme == .dark ? AppColors.textInverse.opacity(0.9) : AppColors.textPrimary)
Button("Criar conta") {
navigate(.registration)
}
.buttonStyle(.plain)
.foregroundStyle(AppColors.primary)
}
.font(AppTypography.body)
.opacity(textVisible ? 1.0 : 0.0)
.offset(y: textVisible ? 0 : 24)
Spacer().frame(height: 12)
SecondaryButton(title: "Entrar") {
navigate(.loginEmail)
}
}
.ignoresSafeArea()
.onAppear {
if shouldPrepareEntryAnimation {
applyHiddenStateWithoutAnimation()
} else {
showFinalStateWithoutAnimation()
}
}
.task(id: authEntryAnimationToken) {
await runEntryAnimationIfNeeded(for: authEntryAnimationToken)
}
.padding(.horizontal, 28)
legalLinks
.padding(.top, 4)
.padding(.bottom, 24)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppColors.backgroundLight.ignoresSafeArea())
}
@MainActor
private func applyHiddenStateWithoutAnimation() {
heroVisible = false
textVisible = false
buttonVisible = false
}
@MainActor
private func showFinalStateWithoutAnimation() {
heroVisible = true
textVisible = true
buttonVisible = true
}
@MainActor
private func runEntryAnimationIfNeeded(for token: Int) async {
guard token > 0 else { return }
guard token != lastAnimatedToken else { return }
lastAnimatedToken = token
applyHiddenStateWithoutAnimation()
try? await Task.sleep(nanoseconds: 40_000_000)
withAnimation(.spring(response: 0.64, dampingFraction: 0.9)) {
heroVisible = true
}
try? await Task.sleep(nanoseconds: 160_000_000)
withAnimation(.easeOut(duration: 0.42)) {
textVisible = true
}
try? await Task.sleep(nanoseconds: 150_000_000)
withAnimation(.spring(response: 0.52, dampingFraction: 0.86)) {
buttonVisible = true
private var legalLinks: some View {
HStack(spacing: 6) {
Button("Termos de Uso") { navigate(.terms) }
.buttonStyle(.plain)
Text("·")
Button("Política de Privacidade") { navigate(.policy) }
.buttonStyle(.plain)
}
.font(.system(size: 14, weight: .medium))
.foregroundStyle(AppColors.textMuted)
}
}
#Preview {
LoginView(root: .constant(.auth), navigate: { _ in })
}