[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:
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user