diff --git a/PediFoods/ContentView.swift b/PediFoods/ContentView.swift index 3d77f86..254f04c 100644 --- a/PediFoods/ContentView.swift +++ b/PediFoods/ContentView.swift @@ -15,8 +15,6 @@ struct ContentView: View { @State var hasPerformedInitialLaunchBootstrap = false @State var showLaunchSplash = true @State var shouldPulseLaunchSplash = true - @State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil - @State var authEntryAnimationToken = 0 @State private var sessionExpiredObserver: NSObjectProtocol? @State var cartResetObserver: Any? @State var appResumeObserver: Any? @@ -32,9 +30,7 @@ struct ContentView: View { root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, - appState: $appState, - shouldPrepareLoginEntry: shouldPrepareAuthEntryAnimation, - authEntryAnimationToken: authEntryAnimationToken + appState: $appState ) case .main: if isBootstrappingSession { @@ -136,15 +132,11 @@ struct ContentView: View { } if root == .auth { - shouldPrepareAuthEntryAnimation = true shouldPulseLaunchSplash = false try? await Task.sleep(nanoseconds: 180_000_000) withAnimation(.easeInOut(duration: 0.34)) { showLaunchSplash = false } - try? await Task.sleep(nanoseconds: 360_000_000) - authEntryAnimationToken += 1 - scheduleDisableAuthEntryPreparation() return } @@ -410,17 +402,13 @@ struct ContentView: View { enterAuthFlow() } - /// The only correct way to transition `root` to `.auth`: pairs the - /// switch with the token bump `LoginView` needs to run its entry - /// reveal animation. A bare `root = .auth` leaves the login screen's - /// hero/text/button stuck hidden - see - /// decisions/2026-08-06-login-entry-animation-bug.md. + /// The single funnel for entering the auth flow. `LoginView` is a + /// static screen with its own `LCENavigationView` back button (sets + /// `root = .main`), so no animation priming is needed here - see + /// decisions/2026-08-27-payment-auth-lockout-clean-intro-screen.md. @MainActor private func enterAuthFlow() { - shouldPrepareAuthEntryAnimation = true root = .auth - authEntryAnimationToken += 1 - scheduleDisableAuthEntryPreparation() } @MainActor @@ -463,13 +451,6 @@ struct ContentView: View { ) } - private func scheduleDisableAuthEntryPreparation() { - Task { @MainActor in - try? await Task.sleep(nanoseconds: 900_000_000) - shouldPrepareAuthEntryAnimation = false - } - } - private func hasConfiguredAddress() -> Bool { if appState.address.selectedId != nil { return true diff --git a/PediFoods/Resources/Localizable.xcstrings b/PediFoods/Resources/Localizable.xcstrings index 8452e20..7099a48 100644 --- a/PediFoods/Resources/Localizable.xcstrings +++ b/PediFoods/Resources/Localizable.xcstrings @@ -88,6 +88,9 @@ }, "Abrir Ajustes" : { + }, + "Acesse sua conta" : { + }, "Acompanhamento em tempo real" : { @@ -346,10 +349,6 @@ "comment" : "A prompt asking users to share their opinion about the delivery.", "isCommentAutoGenerated" : true }, - "Criar conta" : { - "comment" : "A link that navigates to the registration screen.", - "isCommentAutoGenerated" : true - }, "Crie sua conta" : { "comment" : "A link that directs users to create an account.", "isCommentAutoGenerated" : true @@ -748,9 +747,6 @@ "Não recebeu o código?" : { "comment" : "A question displayed below the button to re-send the OTP.", "isCommentAutoGenerated" : true - }, - "Não tem conta ainda?" : { - }, "Nenhum cartão cadastrado" : { diff --git a/PediFoods/Views/Auth/AuthFlowView.swift b/PediFoods/Views/Auth/AuthFlowView.swift index 1dc0aa9..9edd55f 100644 --- a/PediFoods/Views/Auth/AuthFlowView.swift +++ b/PediFoods/Views/Auth/AuthFlowView.swift @@ -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 diff --git a/PediFoods/Views/Auth/LoginView.swift b/PediFoods/Views/Auth/LoginView.swift index 6d79a9e..dfa36a3 100644 --- a/PediFoods/Views/Auth/LoginView.swift +++ b/PediFoods/Views/Auth/LoginView.swift @@ -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 }) +} diff --git a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift index 0f4f85d..2445888 100644 --- a/PediFoodsUITests/ProfileLoggedOutFlowTests.swift +++ b/PediFoodsUITests/ProfileLoggedOutFlowTests.swift @@ -1,10 +1,7 @@ import XCTest -/// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) - -/// confirmed reachable. Actually logging in is blocked by a separate -/// navigation issue in LoginView's entry-reveal animation, documented in -/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md - not -/// covered here. +/// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) and +/// the auth intro screen (`LoginView`) it opens. final class ProfileLoggedOutFlowTests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false @@ -20,18 +17,27 @@ final class ProfileLoggedOutFlowTests: XCTestCase { XCTAssertTrue(app.buttons["Entrar ou Cadastrar"].isHittable) } - func testTappingLoginButtonDoesNotCrash() throws { + /// The auth intro screen must offer both entry points plus the legal + /// links, and — the reason App Review rejected the build in 2026-08 — + /// a working way back out (its `LCENavigationView` back button, which + /// sets `root = .main`). + func testAuthIntroScreenShowsChoicesAndCanGoBack() throws { let app = XCUIApplication() app.launch() XCTAssertTrue(app.reachLoggedOutProfile()) app.buttons["Entrar ou Cadastrar"].tap() - sleep(2) - // Known limitation (see decisions doc): the login screen's entry - // animation doesn't reliably reveal when navigating here from an - // already-running guest session, so this only asserts the app is - // still alive and responsive, not that login itself completes. - XCTAssertEqual(app.state, .runningForeground) + XCTAssertTrue(app.buttons["Criar conta"].waitForExistence(timeout: 5), "Auth intro missing 'Criar conta'") + XCTAssertTrue(app.buttons["Entrar"].exists, "Auth intro missing 'Entrar'") + XCTAssertTrue(app.buttons["Termos de Uso"].exists, "Auth intro missing 'Termos de Uso' link") + XCTAssertTrue(app.buttons["Política de Privacidade"].exists, "Auth intro missing 'Política de Privacidade' link") + + let backButton = app.buttons.matching(NSPredicate(format: "label == %@", "Back")).firstMatch + XCTAssertTrue(backButton.exists, "Auth intro has no back button — user is trapped") + backButton.tap() + + // 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") } } diff --git a/PediFoodsUITests/UITestSupport.swift b/PediFoodsUITests/UITestSupport.swift index 52ff997..b4db582 100644 --- a/PediFoodsUITests/UITestSupport.swift +++ b/PediFoodsUITests/UITestSupport.swift @@ -147,7 +147,7 @@ extension XCUIApplication { buttons["Entrar ou Cadastrar"].tap() - let entrarButton = buttons["ENTRAR"] + let entrarButton = buttons["Entrar"] guard entrarButton.waitForExistence(timeout: 5) else { return false } entrarButton.tap()