feat(android): complete first successful skip export
This commit is contained in:
@@ -11,7 +11,9 @@ struct CachedRemoteImage<Placeholder: View>: View {
|
|||||||
let ttl: TimeInterval
|
let ttl: TimeInterval
|
||||||
let placeholder: Placeholder
|
let placeholder: Placeholder
|
||||||
|
|
||||||
|
#if canImport(UIKit) || canImport(AppKit)
|
||||||
@StateObject var loader = CachedRemoteImageLoader()
|
@StateObject var loader = CachedRemoteImageLoader()
|
||||||
|
#endif
|
||||||
|
|
||||||
init(
|
init(
|
||||||
imageURL: String?,
|
imageURL: String?,
|
||||||
@@ -60,14 +62,19 @@ struct CachedRemoteImage<Placeholder: View>: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
#if canImport(UIKit) || canImport(AppKit)
|
||||||
loader.load(imageURL: imageURL, ttl: ttl)
|
loader.load(imageURL: imageURL, ttl: ttl)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.onChange(of: imageURL) { _, newValue in
|
.onChange(of: imageURL) { _, newValue in
|
||||||
|
#if canImport(UIKit) || canImport(AppKit)
|
||||||
loader.load(imageURL: newValue, ttl: ttl)
|
loader.load(imageURL: newValue, ttl: ttl)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if canImport(UIKit) || canImport(AppKit)
|
||||||
@MainActor
|
@MainActor
|
||||||
final class CachedRemoteImageLoader: ObservableObject {
|
final class CachedRemoteImageLoader: ObservableObject {
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
@@ -174,3 +181,4 @@ final class CachedRemoteImageLoader: ObservableObject {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ struct SnackbarOverlay: View {
|
|||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(message.style.backgroundColor)
|
.background(message.style.backgroundColor)
|
||||||
.contentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
center.handleTap()
|
center.handleTap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct ContentView: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@State private var sessionExpiredObserver: NSObjectProtocol?
|
@State private var sessionExpiredObserver: NSObjectProtocol?
|
||||||
#endif
|
#endif
|
||||||
|
@State var cartResetObserver: Any?
|
||||||
#if os(Android)
|
#if os(Android)
|
||||||
@State var snackbarCenter = SnackbarCenter.shared
|
@State var snackbarCenter = SnackbarCenter.shared
|
||||||
#else
|
#else
|
||||||
@@ -69,9 +70,6 @@ struct ContentView: View {
|
|||||||
.onChange(of: appState.address.longitude) { _, _ in
|
.onChange(of: appState.address.longitude) { _, _ in
|
||||||
dismissAddressPickerIfAddressExists()
|
dismissAddressPickerIfAddressExists()
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .cartDidReset)) { _ in
|
|
||||||
appState.cart = CartState()
|
|
||||||
}
|
|
||||||
.task {
|
.task {
|
||||||
await performInitialLaunchBootstrap()
|
await performInitialLaunchBootstrap()
|
||||||
}
|
}
|
||||||
@@ -83,11 +81,13 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
attachCartResetObserverIfNeeded()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
attachSessionExpiredObserverIfNeeded()
|
attachSessionExpiredObserverIfNeeded()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
detachCartResetObserver()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
detachSessionExpiredObserver()
|
detachSessionExpiredObserver()
|
||||||
#endif
|
#endif
|
||||||
@@ -182,6 +182,23 @@ struct ContentView: View {
|
|||||||
isBootstrappingSession = false
|
isBootstrappingSession = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func attachCartResetObserverIfNeeded() {
|
||||||
|
guard cartResetObserver == nil else { return }
|
||||||
|
cartResetObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: .cartDidReset,
|
||||||
|
object: nil,
|
||||||
|
queue: nil
|
||||||
|
) { _ in
|
||||||
|
appState.cart = CartState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func detachCartResetObserver() {
|
||||||
|
guard let cartResetObserver else { return }
|
||||||
|
NotificationCenter.default.removeObserver(cartResetObserver)
|
||||||
|
self.cartResetObserver = nil
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func hydrateAppState(with customer: CustomerProfile) {
|
private func hydrateAppState(with customer: CustomerProfile) {
|
||||||
appState.profile.id = customer.id
|
appState.profile.id = customer.id
|
||||||
|
|||||||
@@ -76,18 +76,18 @@ final class OrderRealtimeTracker {
|
|||||||
@discardableResult
|
@discardableResult
|
||||||
private func fetchLatest(orderId: String) async -> PublicOrderResult? {
|
private func fetchLatest(orderId: String) async -> PublicOrderResult? {
|
||||||
do {
|
do {
|
||||||
logger.debug("OrderTracking poll request orderId=\(orderId, privacy: .public)")
|
logger.debug("OrderTracking poll request orderId=\(orderId)")
|
||||||
let response = try await ApiService().publicOrder(orderId: orderId)
|
let response = try await ApiService().publicOrder(orderId: orderId)
|
||||||
guard response.error == false, let order = response.result else {
|
guard response.error == false, let order = response.result else {
|
||||||
logger.error("OrderTracking poll API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
|
logger.error("OrderTracking poll API error orderId=\(orderId) message=\(response.message ?? "unknown")")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
clearPendingCartIfNeeded(for: order)
|
clearPendingCartIfNeeded(for: order)
|
||||||
logger.info("OrderTracking poll success orderId=\(orderId, privacy: .public) status=\((order.status ?? "nil"), privacy: .public) paymentStatus=\((order.paymentStatus ?? "nil"), privacy: .public)")
|
logger.info("OrderTracking poll success orderId=\(orderId) status=\(order.status ?? "nil") paymentStatus=\(order.paymentStatus ?? "nil")")
|
||||||
onOrderUpdated?(order)
|
onOrderUpdated?(order)
|
||||||
return order
|
return order
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("OrderTracking poll failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
logger.error("OrderTracking poll failure orderId=\(orderId) error=\(error.localizedDescription)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import UIKit
|
|||||||
|
|
||||||
#if !canImport(UIKit)
|
#if !canImport(UIKit)
|
||||||
struct UIDevice {
|
struct UIDevice {
|
||||||
static let topNotch: CGFloat = 0
|
static let topNotch = 0.0
|
||||||
static let bottomNotch: CGFloat = 0
|
static let bottomNotch = 0.0
|
||||||
|
|
||||||
var modelName: String {
|
var modelName: String {
|
||||||
"android"
|
"android"
|
||||||
@@ -67,6 +67,37 @@ extension View {
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func appContentShape<S: Shape>(_ shape: S) -> some View {
|
||||||
|
#if os(Android)
|
||||||
|
self
|
||||||
|
#else
|
||||||
|
self.contentShape(shape)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func appBottomSafeAreaInset<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||||
|
#if os(Android)
|
||||||
|
self.overlay(alignment: .bottom) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
self.safeAreaInset(edge: .bottom) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func appLayoutPriority(_ value: Double) -> some View {
|
||||||
|
#if os(Android)
|
||||||
|
self
|
||||||
|
#else
|
||||||
|
self.layoutPriority(value)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
import SkipFuse
|
|
||||||
|
|
||||||
/// The Observable ViewModel used by the application.
|
|
||||||
@Observable public class ViewModel {
|
|
||||||
var items: [Item] = loadItems() {
|
|
||||||
didSet { saveItems() }
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func clear() {
|
|
||||||
items.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUpdated(_ item: Item) -> Bool {
|
|
||||||
item != items.first { i in
|
|
||||||
i.id == item.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save(item: Item) {
|
|
||||||
items = items.map { i in
|
|
||||||
i.id == item.id ? item : i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An individual item held by the ViewModel
|
|
||||||
struct Item : Identifiable, Hashable, Codable {
|
|
||||||
let id: UUID
|
|
||||||
var date: Date
|
|
||||||
var favorite: Bool
|
|
||||||
var title: String
|
|
||||||
var notes: String
|
|
||||||
|
|
||||||
init(id: UUID = UUID(), date: Date = .now, favorite: Bool = false, title: String = "", notes: String = "") {
|
|
||||||
self.id = id
|
|
||||||
self.date = date
|
|
||||||
self.favorite = favorite
|
|
||||||
self.title = title
|
|
||||||
self.notes = notes
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemTitle: String {
|
|
||||||
!title.isEmpty ? title : dateString
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateString: String {
|
|
||||||
date.formatted(date: .complete, time: .omitted)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateTimeString: String {
|
|
||||||
date.formatted(date: .abbreviated, time: .shortened)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utilities for defaulting and persising the items in the list
|
|
||||||
extension ViewModel {
|
|
||||||
private static let savePath = URL.applicationSupportDirectory.appendingPathComponent("appdata.json")
|
|
||||||
|
|
||||||
fileprivate static func loadItems() -> [Item] {
|
|
||||||
do {
|
|
||||||
let start = Date.now
|
|
||||||
let data = try Data(contentsOf: savePath)
|
|
||||||
defer {
|
|
||||||
let end = Date.now
|
|
||||||
logger.info("loaded \(data.count) bytes from \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
|
|
||||||
}
|
|
||||||
return try JSONDecoder().decode([Item].self, from: data)
|
|
||||||
} catch {
|
|
||||||
// perhaps the first launch, or the data could not be read
|
|
||||||
logger.warning("failed to load data from \(Self.savePath), using defaultItems: \(error)")
|
|
||||||
let defaultItems = (1...365).map { Date(timeIntervalSinceNow: Double($0 * 60 * 60 * 24 * -1)) }
|
|
||||||
return defaultItems.map({ Item(date: $0) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func saveItems() {
|
|
||||||
do {
|
|
||||||
let start = Date.now
|
|
||||||
let data = try JSONEncoder().encode(items)
|
|
||||||
try FileManager.default.createDirectory(at: URL.applicationSupportDirectory, withIntermediateDirectories: true)
|
|
||||||
try data.write(to: Self.savePath)
|
|
||||||
let end = Date.now
|
|
||||||
logger.info("saved \(data.count) bytes to \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
|
|
||||||
} catch {
|
|
||||||
logger.error("error saving data: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -114,25 +114,17 @@ struct LoginView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func applyHiddenStateWithoutAnimation() {
|
private func applyHiddenStateWithoutAnimation() {
|
||||||
var transaction = Transaction()
|
|
||||||
transaction.disablesAnimations = true
|
|
||||||
withTransaction(transaction) {
|
|
||||||
heroVisible = false
|
heroVisible = false
|
||||||
textVisible = false
|
textVisible = false
|
||||||
buttonVisible = false
|
buttonVisible = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func showFinalStateWithoutAnimation() {
|
private func showFinalStateWithoutAnimation() {
|
||||||
var transaction = Transaction()
|
|
||||||
transaction.disablesAnimations = true
|
|
||||||
withTransaction(transaction) {
|
|
||||||
heroVisible = true
|
heroVisible = true
|
||||||
textVisible = true
|
textVisible = true
|
||||||
buttonVisible = true
|
buttonVisible = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func runEntryAnimationIfNeeded(for token: Int) async {
|
private func runEntryAnimationIfNeeded(for token: Int) async {
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ struct AddressCard: View {
|
|||||||
.font(AppTypography.heading2)
|
.font(AppTypography.heading2)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
|
||||||
.minimumScaleFactor(0.9)
|
.minimumScaleFactor(0.9)
|
||||||
|
|
||||||
if item.isPrimary {
|
if item.isPrimary {
|
||||||
@@ -137,9 +136,7 @@ struct AddressCard: View {
|
|||||||
.font(AppTypography.overline)
|
.font(AppTypography.overline)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9))
|
||||||
.padding(.horizontal, 9)
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
.background(AppColors.tertiary)
|
.background(AppColors.tertiary)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ struct AddressesView: View {
|
|||||||
item: addressToListItem(address, isPrimary: isSelected),
|
item: addressToListItem(address, isPrimary: isSelected),
|
||||||
onEdit: { beginEditing(address) }
|
onEdit: { beginEditing(address) }
|
||||||
)
|
)
|
||||||
.contentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if openSwipeRowId == rowId {
|
if openSwipeRowId == rowId {
|
||||||
openSwipeRowId = nil
|
openSwipeRowId = nil
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ struct CheckoutView: View {
|
|||||||
.background(AppColors.backgroundLight)
|
.background(AppColors.backgroundLight)
|
||||||
.navigationTitle("Finalizar Pedido")
|
.navigationTitle("Finalizar Pedido")
|
||||||
.appInlineNavigationTitle()
|
.appInlineNavigationTitle()
|
||||||
.safeAreaInset(edge: .bottom) {
|
.appBottomSafeAreaInset {
|
||||||
bottomBar
|
bottomBar
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
@@ -433,7 +433,7 @@ struct CheckoutView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 14)
|
.padding(.horizontal, 14)
|
||||||
.padding(.top, 14)
|
.padding(.top, 14)
|
||||||
.contentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
.onTapGesture(perform: onTap)
|
.onTapGesture(perform: onTap)
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import LCEssentials
|
import LCEssentials
|
||||||
@@ -451,18 +450,26 @@ struct HomeView: View {
|
|||||||
let display = appState.address.display
|
let display = appState.address.display
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.lowercased()
|
.lowercased()
|
||||||
let lat = appState.address.latitude.map { String(format: "%.5f", $0) } ?? "nil"
|
let lat = appState.address.latitude.map(formatCoordinateScope) ?? "nil"
|
||||||
let lng = appState.address.longitude.map { String(format: "%.5f", $0) } ?? "nil"
|
let lng = appState.address.longitude.map(formatCoordinateScope) ?? "nil"
|
||||||
return "\(selected)|\(display)|\(lat)|\(lng)"
|
return "\(selected)|\(display)|\(lat)|\(lng)"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func homeStoresCacheKey(lat: Double?, lng: Double?, category: String?) -> String {
|
private func homeStoresCacheKey(lat: Double?, lng: Double?, category: String?) -> String {
|
||||||
let normalizedCategory = category?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "all"
|
let normalizedCategory = category?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "all"
|
||||||
let latKey = lat.map { String(format: "%.4f", $0) } ?? "nil"
|
let latKey = lat.map(formatCoordinateCache) ?? "nil"
|
||||||
let lngKey = lng.map { String(format: "%.4f", $0) } ?? "nil"
|
let lngKey = lng.map(formatCoordinateCache) ?? "nil"
|
||||||
return "stores:\(latKey):\(lngKey):\(normalizedCategory)"
|
return "stores:\(latKey):\(lngKey):\(normalizedCategory)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func formatCoordinateScope(_ value: Double) -> String {
|
||||||
|
String((value * 100_000).rounded() / 100_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatCoordinateCache(_ value: Double) -> String {
|
||||||
|
String((value * 10_000).rounded() / 10_000)
|
||||||
|
}
|
||||||
|
|
||||||
private var selectedCategoryQueryValue: String? {
|
private var selectedCategoryQueryValue: String? {
|
||||||
guard selectedCategory.lowercased() != "all" else { return nil }
|
guard selectedCategory.lowercased() != "all" else { return nil }
|
||||||
guard let selected = categories.first(where: { $0.id == selectedCategory }) else { return nil }
|
guard let selected = categories.first(where: { $0.id == selectedCategory }) else { return nil }
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct OrderDetailsView: View {
|
|||||||
.background(AppColors.backgroundLight)
|
.background(AppColors.backgroundLight)
|
||||||
.navigationTitle("Detalhes do Pedido")
|
.navigationTitle("Detalhes do Pedido")
|
||||||
.appInlineNavigationTitle()
|
.appInlineNavigationTitle()
|
||||||
.safeAreaInset(edge: .bottom) {
|
.appBottomSafeAreaInset {
|
||||||
VStack {
|
VStack {
|
||||||
reorderButton
|
reorderButton
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct OrderTrackingView: View {
|
|||||||
@State var showCancellationReason = false
|
@State var showCancellationReason = false
|
||||||
@State var reviewDraft: ReviewDraft? = nil
|
@State var reviewDraft: ReviewDraft? = nil
|
||||||
@State var didSaveReviewForCurrentOrder = false
|
@State var didSaveReviewForCurrentOrder = false
|
||||||
|
@State var reviewSavedObserver: Any?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
@@ -61,21 +62,41 @@ struct OrderTrackingView: View {
|
|||||||
}
|
}
|
||||||
tracker.start(orderId: orderId, jwt: DefaultTokenStore().jwt)
|
tracker.start(orderId: orderId, jwt: DefaultTokenStore().jwt)
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
attachReviewSavedObserverIfNeeded()
|
||||||
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
tracker.stop()
|
tracker.stop()
|
||||||
|
detachReviewSavedObserver()
|
||||||
}
|
}
|
||||||
.navigationDestination(item: $reviewDraft) { draft in
|
.navigationDestination(item: $reviewDraft) { draft in
|
||||||
MyReviewsView(initialOrder: draft)
|
MyReviewsView(initialOrder: draft)
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .orderReviewDidSave)) { payload in
|
}
|
||||||
|
|
||||||
|
private func attachReviewSavedObserverIfNeeded() {
|
||||||
|
guard reviewSavedObserver == nil else { return }
|
||||||
|
reviewSavedObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: .orderReviewDidSave,
|
||||||
|
object: nil,
|
||||||
|
queue: nil
|
||||||
|
) { payload in
|
||||||
guard let orderIdFromEvent = payload.userInfo?["orderId"] as? String else { return }
|
guard let orderIdFromEvent = payload.userInfo?["orderId"] as? String else { return }
|
||||||
let currentOrderId = (order?.id ?? orderId).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let currentOrderId = (order?.id ?? orderId)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
if orderIdFromEvent.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == currentOrderId {
|
if orderIdFromEvent.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == currentOrderId {
|
||||||
didSaveReviewForCurrentOrder = true
|
didSaveReviewForCurrentOrder = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func detachReviewSavedObserver() {
|
||||||
|
guard let reviewSavedObserver else { return }
|
||||||
|
NotificationCenter.default.removeObserver(reviewSavedObserver)
|
||||||
|
self.reviewSavedObserver = nil
|
||||||
|
}
|
||||||
|
|
||||||
private var topHeader: some View {
|
private var topHeader: some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Circle()
|
Circle()
|
||||||
@@ -754,23 +775,23 @@ struct OrderTrackingView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadInitialOrder() async {
|
private func loadInitialOrder() async {
|
||||||
logger.info("OrderTracking initial fetch orderId=\(orderId, privacy: .public)")
|
logger.info("OrderTracking initial fetch orderId=\(orderId)")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let response = try await ApiService().publicOrder(orderId: orderId)
|
let response = try await ApiService().publicOrder(orderId: orderId)
|
||||||
if response.error {
|
if response.error {
|
||||||
errorMessage = response.message ?? "Não foi possível carregar o pedido."
|
errorMessage = response.message ?? "Não foi possível carregar o pedido."
|
||||||
logger.error("OrderTracking initial fetch API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
|
logger.error("OrderTracking initial fetch API error orderId=\(orderId) message=\(response.message ?? "unknown")")
|
||||||
} else if let result = response.result {
|
} else if let result = response.result {
|
||||||
order = result
|
order = result
|
||||||
storeContactPhone = result.storePhone
|
storeContactPhone = result.storePhone
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
logger.info("OrderTracking initial fetch success orderId=\(orderId, privacy: .public) status=\((result.status ?? "nil"), privacy: .public) paymentStatus=\((result.paymentStatus ?? "nil"), privacy: .public)")
|
logger.info("OrderTracking initial fetch success orderId=\(orderId) status=\(result.status ?? "nil") paymentStatus=\(result.paymentStatus ?? "nil")")
|
||||||
await refreshStoreContactPhone(for: result)
|
await refreshStoreContactPhone(for: result)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Não foi possível carregar o pedido."
|
errorMessage = "Não foi possível carregar o pedido."
|
||||||
logger.error("OrderTracking initial fetch failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
logger.error("OrderTracking initial fetch failure orderId=\(orderId) error=\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ struct OrdersView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.5)
|
.minimumScaleFactor(0.5)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.layoutPriority(0)
|
.appLayoutPriority(0)
|
||||||
|
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ struct OrdersView: View {
|
|||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.layoutPriority(2)
|
.appLayoutPriority(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(18)
|
.padding(18)
|
||||||
@@ -719,16 +719,16 @@ struct OrderEntryDestinationView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func fetchOrderForRouting() async -> PublicOrderResult? {
|
func fetchOrderForRouting() async -> PublicOrderResult? {
|
||||||
logger.info("OrderEntry fetch route orderId=\(orderId, privacy: .public)")
|
logger.info("OrderEntry fetch route orderId=\(orderId)")
|
||||||
do {
|
do {
|
||||||
let response = try await ApiService().publicOrder(orderId: orderId)
|
let response = try await ApiService().publicOrder(orderId: orderId)
|
||||||
if response.error == false, let result = response.result {
|
if response.error == false, let result = response.result {
|
||||||
logger.info("OrderEntry fetch route success orderId=\(orderId, privacy: .public) status=\((result.status ?? "nil"), privacy: .public) paymentStatus=\((result.paymentStatus ?? "nil"), privacy: .public)")
|
logger.info("OrderEntry fetch route success orderId=\(orderId) status=\(result.status ?? "nil") paymentStatus=\(result.paymentStatus ?? "nil")")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
logger.error("OrderEntry fetch route API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)")
|
logger.error("OrderEntry fetch route API error orderId=\(orderId) message=\(response.message ?? "unknown")")
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("OrderEntry fetch route failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
|
logger.error("OrderEntry fetch route failure orderId=\(orderId) error=\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ extension PizzaProductDetailSheet {
|
|||||||
.font(AppTypography.heading3)
|
.font(AppTypography.heading3)
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
ForEach(Array(sizeItems.indices), id: \.self) { index in
|
ForEach(0..<sizeItems.count, id: \.self) { index in
|
||||||
sizeRow(sizeItems[index])
|
sizeRow(sizeItems[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ extension PizzaProductDetailSheet {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
|
||||||
ForEach(Array(flavorItems.indices), id: \.self) { index in
|
ForEach(0..<flavorItems.count, id: \.self) { index in
|
||||||
let flavor = flavorItems[index]
|
let flavor = flavorItems[index]
|
||||||
let isSelected = selectedFlavorIds.contains(flavor.id)
|
let isSelected = selectedFlavorIds.contains(flavor.id)
|
||||||
let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price
|
let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price
|
||||||
@@ -118,7 +118,7 @@ extension PizzaProductDetailSheet {
|
|||||||
.disabled(disableSwitch)
|
.disabled(disableSwitch)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.contentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
guard isSelected else { return }
|
guard isSelected else { return }
|
||||||
guard flavor.addonGroups.contains(where: { $0.items.isEmpty == false }) else { return }
|
guard flavor.addonGroups.contains(where: { $0.items.isEmpty == false }) else { return }
|
||||||
@@ -172,7 +172,7 @@ extension PizzaProductDetailSheet {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ struct PizzaProductDetailSheet: View {
|
|||||||
.padding(20)
|
.padding(20)
|
||||||
.padding(.bottom, 90)
|
.padding(.bottom, 90)
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .bottom) {
|
.appBottomSafeAreaInset {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Button(action: { if quantity > 1 { quantity -= 1 } }) {
|
Button(action: { if quantity > 1 { quantity -= 1 } }) {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ struct ProductDetailSheet: View {
|
|||||||
.padding(20)
|
.padding(20)
|
||||||
.padding(.bottom, 80)
|
.padding(.bottom, 80)
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .bottom) {
|
.appBottomSafeAreaInset {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Button(action: { if quantity > 0 { quantity -= 1 } }) {
|
Button(action: { if quantity > 0 { quantity -= 1 } }) {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ struct ProfileView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(edges: .top)
|
.ignoresSafeArea(edges: .top)
|
||||||
.safeAreaInset(edge: .bottom) {
|
.appBottomSafeAreaInset {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(AppColors.backgroundLight.opacity(0.8))
|
.fill(AppColors.backgroundLight.opacity(0.8))
|
||||||
.frame(height: tabBarClearance)
|
.frame(height: tabBarClearance)
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ struct StoreDetailView: View {
|
|||||||
AppColors.backgroundLight.ignoresSafeArea()
|
AppColors.backgroundLight.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
|
#if os(Android)
|
||||||
|
LazyVStack(spacing: 0) {
|
||||||
|
topSection
|
||||||
|
|
||||||
|
categoryTabs(proxy: proxy, safeTop: safeTop, isSticky: false)
|
||||||
|
|
||||||
|
sectionedProducts
|
||||||
|
}
|
||||||
|
#else
|
||||||
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
|
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
|
||||||
topSection
|
topSection
|
||||||
|
|
||||||
@@ -57,6 +66,7 @@ struct StoreDetailView: View {
|
|||||||
|
|
||||||
sectionedProducts
|
sectionedProducts
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await loadStoreData(forceRefresh: true)
|
await loadStoreData(forceRefresh: true)
|
||||||
@@ -472,7 +482,7 @@ struct StoreDetailView: View {
|
|||||||
}
|
}
|
||||||
.offset(x: 3, y: -3)
|
.offset(x: 3, y: -3)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.contentShape(Circle())
|
.appContentShape(Circle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
@@ -484,7 +494,7 @@ struct StoreDetailView: View {
|
|||||||
.padding(12)
|
.padding(12)
|
||||||
.background(AppColors.surface)
|
.background(AppColors.surface)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
.contentShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
.appContentShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
guard isStoreOpen else {
|
guard isStoreOpen else {
|
||||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#if os(Android)
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UserProfileView: View {
|
||||||
|
@Binding var appState: AppState
|
||||||
|
|
||||||
|
@State var name: String = ""
|
||||||
|
@State var email: String = ""
|
||||||
|
@State var phone: String = ""
|
||||||
|
@State var profilePicture: String = ""
|
||||||
|
@State var isSaving = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 22) {
|
||||||
|
avatarSection
|
||||||
|
formSection
|
||||||
|
saveButton
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.top, 18)
|
||||||
|
.padding(.bottom, UIDevice.bottomNotch + 24)
|
||||||
|
}
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
.navigationTitle("Meu Perfil")
|
||||||
|
.appInlineNavigationTitle()
|
||||||
|
.onAppear {
|
||||||
|
hydrateFromAppState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarSection: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Circle()
|
||||||
|
.fill(AppColors.brandSoft)
|
||||||
|
.frame(width: 110, height: 110)
|
||||||
|
.overlay {
|
||||||
|
Text(initials)
|
||||||
|
.font(.system(size: 32, weight: .bold))
|
||||||
|
.foregroundStyle(AppColors.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Remover") {
|
||||||
|
profilePicture = ""
|
||||||
|
}
|
||||||
|
.font(AppTypography.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(profilePicture.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var formSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
textFieldSection(title: "Nome", placeholder: "Seu nome completo", text: $name)
|
||||||
|
|
||||||
|
textFieldSection(title: "E-mail", placeholder: "seu@email.com", text: $email)
|
||||||
|
.appNoAutoCap()
|
||||||
|
|
||||||
|
textFieldSection(title: "Telefone", placeholder: "(00) 00000-0000", text: $phone)
|
||||||
|
.onChange(of: phone) { _, newValue in
|
||||||
|
let masked = formatPhoneBR(displayPhoneDigits(newValue))
|
||||||
|
if masked != newValue {
|
||||||
|
phone = masked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.appNoAutoCap()
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(AppColors.surface)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var saveButton: some View {
|
||||||
|
Button(isSaving ? "Salvando..." : "Salvar Alterações") {
|
||||||
|
Task { await saveProfile() }
|
||||||
|
}
|
||||||
|
.font(AppTypography.button)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 56)
|
||||||
|
.background(AppColors.primary)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(isSaving || canSave == false)
|
||||||
|
.opacity((isSaving || canSave == false) ? 0.6 : 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var initials: String {
|
||||||
|
let parts = name
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.split(separator: " ")
|
||||||
|
.prefix(2)
|
||||||
|
let letters = parts.compactMap { $0.first }.map(String.init).joined()
|
||||||
|
return letters.isEmpty ? "PF" : letters.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var canSave: Bool {
|
||||||
|
let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let normalizedPhone = normalizePhoneNumberForAPI(phone)
|
||||||
|
return cleanName.isEmpty == false
|
||||||
|
&& cleanEmail.isEmpty == false
|
||||||
|
&& cleanEmail.contains("@")
|
||||||
|
&& normalizedPhone.isEmpty == false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hydrateFromAppState() {
|
||||||
|
name = appState.profile.name
|
||||||
|
email = appState.profile.email
|
||||||
|
phone = formatPhoneForDisplay(appState.profile.phone)
|
||||||
|
profilePicture = appState.profile.profilePicture
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func saveProfile() async {
|
||||||
|
guard canSave else { return }
|
||||||
|
|
||||||
|
let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let normalizedPhone = normalizePhoneNumberForAPI(phone)
|
||||||
|
let cleanPhoto = profilePicture.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
isSaving = true
|
||||||
|
defer { isSaving = false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await ApiService().updateCustomerProfile(
|
||||||
|
name: cleanName,
|
||||||
|
email: cleanEmail,
|
||||||
|
phoneNumber: normalizedPhone,
|
||||||
|
profilePicture: cleanPhoto.isEmpty ? nil : cleanPhoto
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.error {
|
||||||
|
SnackbarCenter.shared.show(
|
||||||
|
title: response.message ?? "Não foi possível atualizar seu perfil.",
|
||||||
|
style: .error,
|
||||||
|
icon: "xmark.octagon.fill",
|
||||||
|
duration: 3.0
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let customer = response.result
|
||||||
|
appState.profile.id = customer?.id ?? appState.profile.id
|
||||||
|
appState.profile.name = customer?.name ?? cleanName
|
||||||
|
appState.profile.email = customer?.email ?? cleanEmail
|
||||||
|
appState.profile.phone = customer?.phoneNumber ?? normalizedPhone
|
||||||
|
appState.profile.profilePicture = customer?.profilePicture ?? cleanPhoto
|
||||||
|
SessionStateStore.setActiveUserKey(
|
||||||
|
SessionStateStore.makeUserKey(profileId: appState.profile.id, email: appState.profile.email)
|
||||||
|
)
|
||||||
|
|
||||||
|
SnackbarCenter.shared.show(
|
||||||
|
title: "Perfil atualizado com sucesso.",
|
||||||
|
style: .success,
|
||||||
|
icon: "checkmark.circle.fill",
|
||||||
|
duration: 2.0
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
SnackbarCenter.shared.show(
|
||||||
|
title: "Não foi possível atualizar seu perfil.",
|
||||||
|
style: .error,
|
||||||
|
icon: "xmark.octagon.fill",
|
||||||
|
duration: 3.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatPhoneForDisplay(_ raw: String) -> String {
|
||||||
|
let digits = displayPhoneDigits(raw)
|
||||||
|
if digits.isEmpty { return "" }
|
||||||
|
return formatPhoneBR(digits)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayPhoneDigits(_ raw: String) -> String {
|
||||||
|
var digits = raw.filter(\.isNumber)
|
||||||
|
if digits.hasPrefix("55"), digits.count > 11 {
|
||||||
|
digits = String(digits.dropFirst(2))
|
||||||
|
}
|
||||||
|
return String(digits.prefix(11))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func textFieldSection(title: String, placeholder: String, text: Binding<String>) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
|
||||||
|
TextField(placeholder, text: text)
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.frame(height: 50)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
|
.stroke(AppColors.secondary.opacity(0.2), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#if !os(Android)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if canImport(PhotosUI) && os(iOS)
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
@@ -267,3 +268,4 @@ struct UserProfileView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ Legenda:
|
|||||||
1. 🟢 Revisar e blindar imports/dependências iOS-only no código compartilhado.
|
1. 🟢 Revisar e blindar imports/dependências iOS-only no código compartilhado.
|
||||||
2. 🟢 Corrigir o problema de artefato/caminho do ambiente Skip local.
|
2. 🟢 Corrigir o problema de artefato/caminho do ambiente Skip local.
|
||||||
3. 🟢 Validar `swift build` sem erro de ambiente.
|
3. 🟢 Validar `swift build` sem erro de ambiente.
|
||||||
4. 🔴 Validar export/build Android via Skip.
|
4. 🟢 Validar export/build Android via Skip.
|
||||||
5. 🔴 Gerar APK/build Android instalável.
|
5. 🟢 Gerar APK/build Android instalável.
|
||||||
6. 🔴 Instalar e abrir o app no Android.
|
6. 🔴 Instalar e abrir o app no Android.
|
||||||
7. 🔴 Validar login no Android.
|
7. 🔴 Validar login no Android.
|
||||||
8. 🔴 Validar home e navegação principal no Android.
|
8. 🔴 Validar home e navegação principal no Android.
|
||||||
@@ -308,3 +308,13 @@ Legenda:
|
|||||||
2. Task 2 concluída com ajuste local no cache/estado de build do Skip.
|
2. Task 2 concluída com ajuste local no cache/estado de build do Skip.
|
||||||
- Correção local não versionada.
|
- Correção local não versionada.
|
||||||
3. Task 3 concluída com `swift build` finalizado com sucesso.
|
3. Task 3 concluída com `swift build` finalizado com sucesso.
|
||||||
|
4. Task 4 concluída com `skip export --debug --no-ios --android --arch aarch64 --plain --verbose`.
|
||||||
|
- O `skip export` fechou com sucesso completo.
|
||||||
|
- O pipeline passou pela recompilação Swift compartilhada, geração Android Skip, `assembleDebug`, `bundleDebug` e export final.
|
||||||
|
- Artefatos gerados com sucesso em `pedi-foods/.build/skip-export/`:
|
||||||
|
- `PediFoods-debug.apk`
|
||||||
|
- `PediFoods-debug.aab`
|
||||||
|
- `PediFoods-project.zip`
|
||||||
|
5. Task 5 concluída com geração validada de artefatos Android instaláveis.
|
||||||
|
- APK exportado com sucesso.
|
||||||
|
- AAB exportado com sucesso.
|
||||||
|
|||||||
Reference in New Issue
Block a user