Fixs
This commit is contained in:
@@ -6,6 +6,11 @@ struct ContentView: View {
|
||||
private let tokenStore: TokenStore = DefaultTokenStore()
|
||||
@State var appState = AppState()
|
||||
@State var isBootstrappingSession = false
|
||||
@State var hasPerformedInitialLaunchBootstrap = false
|
||||
@State var showLaunchSplash = true
|
||||
@State var shouldPulseLaunchSplash = true
|
||||
@State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil
|
||||
@State var authEntryAnimationToken = 0
|
||||
#if os(iOS)
|
||||
@State private var sessionExpiredObserver: NSObjectProtocol?
|
||||
#endif
|
||||
@@ -20,7 +25,14 @@ struct ContentView: View {
|
||||
Group {
|
||||
switch root {
|
||||
case .auth:
|
||||
AuthFlowView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState)
|
||||
AuthFlowView(
|
||||
root: $root,
|
||||
selectedTab: $selectedTab,
|
||||
tokenStore: tokenStore,
|
||||
appState: $appState,
|
||||
shouldPrepareLoginEntry: shouldPrepareAuthEntryAnimation,
|
||||
authEntryAnimationToken: authEntryAnimationToken
|
||||
)
|
||||
case .main:
|
||||
if isBootstrappingSession {
|
||||
sessionBootstrapLoadingView
|
||||
@@ -31,6 +43,11 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
SnackbarOverlay(center: snackbarCenter)
|
||||
|
||||
if showLaunchSplash {
|
||||
LaunchSplashView(shouldPulse: shouldPulseLaunchSplash)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.sheet(item: $appState.activeModal) { modal in
|
||||
switch modal {
|
||||
@@ -55,8 +72,15 @@ struct ContentView: View {
|
||||
.onReceive(NotificationCenter.default.publisher(for: .cartDidReset)) { _ in
|
||||
appState.cart = CartState()
|
||||
}
|
||||
.task(id: root) {
|
||||
await bootstrapSessionStateIfNeeded()
|
||||
.task {
|
||||
await performInitialLaunchBootstrap()
|
||||
}
|
||||
.onChange(of: root) { _, newValue in
|
||||
if newValue == .main {
|
||||
Task {
|
||||
await bootstrapSessionStateIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
#if os(iOS)
|
||||
@@ -81,6 +105,43 @@ struct ContentView: View {
|
||||
.background(AppColors.backgroundLight)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func performInitialLaunchBootstrap() async {
|
||||
guard hasPerformedInitialLaunchBootstrap == false else { return }
|
||||
hasPerformedInitialLaunchBootstrap = true
|
||||
|
||||
let start = Date()
|
||||
await bootstrapSessionStateIfNeeded()
|
||||
|
||||
// Keep the in-app splash visible long enough to avoid abrupt transition
|
||||
// between native launch screen and app content.
|
||||
let elapsed = Date().timeIntervalSince(start)
|
||||
let minimumSplashDuration: TimeInterval = 1.0
|
||||
if elapsed < minimumSplashDuration {
|
||||
let remaining = minimumSplashDuration - elapsed
|
||||
let nanoseconds = UInt64(remaining * 1_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: nanoseconds)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
shouldPulseLaunchSplash = false
|
||||
withAnimation(.easeOut(duration: 0.28)) {
|
||||
showLaunchSplash = false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapSessionStateIfNeeded() async {
|
||||
guard root == .main else { return }
|
||||
@@ -215,7 +276,17 @@ struct ContentView: View {
|
||||
isBootstrappingSession = false
|
||||
appState = AppState()
|
||||
selectedTab = .home
|
||||
shouldPrepareAuthEntryAnimation = true
|
||||
root = .auth
|
||||
authEntryAnimationToken += 1
|
||||
scheduleDisableAuthEntryPreparation()
|
||||
}
|
||||
|
||||
private func scheduleDisableAuthEntryPreparation() {
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: 900_000_000)
|
||||
shouldPrepareAuthEntryAnimation = false
|
||||
}
|
||||
}
|
||||
|
||||
private func hasConfiguredAddress() -> Bool {
|
||||
|
||||
@@ -1165,7 +1165,7 @@
|
||||
"comment" : "A title for the OTP verification screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Versão 4.20.1" : {
|
||||
"Versão 1.0b" : {
|
||||
"comment" : "The version number of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
|
||||
@@ -82,15 +82,7 @@ final class ApiClient {
|
||||
}
|
||||
|
||||
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||
// NOTE:
|
||||
// /api/customer/login is strict about body fields (email/phoneNumber/otp).
|
||||
// On iOS, route this endpoint through URLSession to preserve raw JSON body behavior.
|
||||
if request.path == "/api/customer/login" {
|
||||
if let body = request.body, let bodyText = String(data: body, encoding: .utf8) {
|
||||
print("[ApiClient] /api/customer/login body: \(bodyText)")
|
||||
}
|
||||
return try await sendWithURLSession(request)
|
||||
}
|
||||
|
||||
#if canImport(LCEssentials) && os(iOS)
|
||||
return try await sendWithLCEssentials(request)
|
||||
#else
|
||||
|
||||
@@ -11,12 +11,21 @@ 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) { route in
|
||||
LoginView(
|
||||
root: $root,
|
||||
selectedTab: $selectedTab,
|
||||
tokenStore: tokenStore,
|
||||
appState: $appState,
|
||||
shouldPrepareEntryAnimation: shouldPrepareLoginEntry,
|
||||
authEntryAnimationToken: authEntryAnimationToken
|
||||
) { route in
|
||||
path.append(route)
|
||||
}
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
|
||||
@@ -6,7 +6,13 @@ struct LoginView: View {
|
||||
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 {
|
||||
#if os(Android)
|
||||
@@ -31,6 +37,7 @@ struct LoginView: View {
|
||||
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)
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
|
||||
@@ -39,7 +46,7 @@ struct LoginView: View {
|
||||
pinHeroImage
|
||||
.scaledToFill()
|
||||
.frame(height: heroHeight + 80)
|
||||
.offset(y: -60)
|
||||
.offset(y: heroVisible ? -60 : -(heroHeight + 220))
|
||||
.mask(
|
||||
LinearGradient(
|
||||
colors: [.black, .black, .black.opacity(0.0)],
|
||||
@@ -49,7 +56,7 @@ struct LoginView: View {
|
||||
)
|
||||
|
||||
VStack(spacing: 18) {
|
||||
Spacer().frame(height: heroHeight - 100)
|
||||
Spacer().frame(height: logoTopInset)
|
||||
|
||||
logoImage
|
||||
.scaledToFit()
|
||||
@@ -61,6 +68,8 @@ struct LoginView: View {
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 28)
|
||||
.opacity(textVisible ? 1.0 : 0.0)
|
||||
.offset(y: textVisible ? 0 : 24)
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
@@ -71,6 +80,8 @@ struct LoginView: View {
|
||||
}
|
||||
.padding(.horizontal, 28)
|
||||
.tint(AppColors.tertiary)
|
||||
.offset(y: buttonVisible ? 0 : 140)
|
||||
.opacity(buttonVisible ? 1.0 : 0.0)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text("Não tem conta ainda?")
|
||||
@@ -81,11 +92,67 @@ struct LoginView: View {
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
.font(AppTypography.body)
|
||||
.opacity(textVisible ? 1.0 : 0.0)
|
||||
.offset(y: textVisible ? 0 : 24)
|
||||
|
||||
Spacer().frame(height: 12)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
if shouldPrepareEntryAnimation {
|
||||
applyHiddenStateWithoutAnimation()
|
||||
} else {
|
||||
showFinalStateWithoutAnimation()
|
||||
}
|
||||
}
|
||||
.task(id: authEntryAnimationToken) {
|
||||
await runEntryAnimationIfNeeded(for: authEntryAnimationToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyHiddenStateWithoutAnimation() {
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = true
|
||||
withTransaction(transaction) {
|
||||
heroVisible = false
|
||||
textVisible = false
|
||||
buttonVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func showFinalStateWithoutAnimation() {
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = true
|
||||
withTransaction(transaction) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LaunchSplashView: View {
|
||||
var shouldPulse: Bool = true
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State var isAnimating = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
let logoTopInset = max(0, (geo.size.height - 180) / 2 - 8)
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer().frame(height: logoTopInset)
|
||||
|
||||
splashLogo
|
||||
.scaledToFit()
|
||||
.frame(width: 180, height: 180)
|
||||
.scaleEffect(isAnimating ? 1.03 : 0.97)
|
||||
.opacity(isAnimating ? 1.0 : 0.9)
|
||||
.animation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true), value: isAnimating)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isAnimating = shouldPulse
|
||||
}
|
||||
.onChange(of: shouldPulse) { _, newValue in
|
||||
isAnimating = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var splashLogo: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ struct HomeView: View {
|
||||
@State var stores: [StoreSummary] = []
|
||||
|
||||
private let specials: [SpecialOfferCardModel] = [
|
||||
.init(id: "ddddd", title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]),
|
||||
.init(id: "eeeee", title: "Enjoy spicy day", subtitle: "Delivery in 20 min", colors: [Color(hex: "#F04B3E"), Color(hex: "#FF8C42")])
|
||||
// .init(id: "ddddd", title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]),
|
||||
// .init(id: "eeeee", title: "Enjoy spicy day", subtitle: "Delivery in 20 min", colors: [Color(hex: "#F04B3E"), Color(hex: "#FF8C42")])
|
||||
]
|
||||
|
||||
private let headerExpandedHeight: CGFloat = 240
|
||||
@@ -123,7 +123,7 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
section(title: "#SpecialForYou") {
|
||||
section(title: "#PediPromo") {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(specials) { item in
|
||||
@@ -135,7 +135,7 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
section(title: "Near you") {
|
||||
section(title: "Pertinho de você") {
|
||||
if isLoadingStores {
|
||||
HStack {
|
||||
ProgressView()
|
||||
@@ -226,7 +226,7 @@ struct HomeView: View {
|
||||
.buttonStyle(.plain)
|
||||
|
||||
VStack(alignment: .center, spacing: 4) {
|
||||
Text("DELIVERY LOCATION")
|
||||
Text("ENTREGAR EM:")
|
||||
.font(AppTypography.overline)
|
||||
.tracking(AppTypography.captionLetterSpacing)
|
||||
.foregroundStyle(AppColors.brandSoft)
|
||||
|
||||
@@ -60,12 +60,12 @@ struct ProfileView: View {
|
||||
// }
|
||||
// .buttonStyle(.plain)
|
||||
|
||||
NavigationLink {
|
||||
Text("Ajuda")
|
||||
} label: {
|
||||
ProfileMenuRow(icon: "gearshape.fill", title: "Configurações")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// NavigationLink {
|
||||
// Text("Ajuda")
|
||||
// } label: {
|
||||
// ProfileMenuRow(icon: "gearshape.fill", title: "Configurações")
|
||||
// }
|
||||
// .buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
@@ -82,7 +82,7 @@ struct ProfileView: View {
|
||||
.padding(.top, 10)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
Text("Versão 4.20.1")
|
||||
Text("Versão 1.0b")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Reference in New Issue
Block a user