bck
This commit is contained in:
@@ -38,7 +38,6 @@
|
||||
496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = "<group>"; };
|
||||
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PediFoods.xcconfig; sourceTree = "<group>"; };
|
||||
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../Intermediates.noindex/BuildToolPluginIntermediates; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
49F90C2B2A52156200F06D93 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Main.swift; path = Sources/Main.swift; sourceTree = SOURCE_ROOT; };
|
||||
49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
|
||||
@@ -66,14 +65,6 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
49AB54462B066A7E007B79B2 /* SkipStone */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
499AB9082B0581F4005E8330 /* plugins */,
|
||||
);
|
||||
name = SkipStone;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
49F90C1F2A52156200F06D93 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -83,7 +74,6 @@
|
||||
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
|
||||
493609562A6B7EAE00C401E2 /* PediFoods */,
|
||||
49F90C2A2A52156200F06D93 /* App */,
|
||||
49AB54462B066A7E007B79B2 /* SkipStone */,
|
||||
496BDBEC2B89A47800C09264 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -110,7 +100,6 @@
|
||||
499CD43A2AC5B799001AE8D8 /* Sources */,
|
||||
499CD43C2AC5B799001AE8D8 /* Frameworks */,
|
||||
499CD43E2AC5B799001AE8D8 /* Resources */,
|
||||
499CD4452AC5B869001AE8D8 /* Run skip gradle */,
|
||||
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
@@ -173,26 +162,6 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
499CD4452AC5B869001AE8D8 /* Run skip gradle */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run skip gradle";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = "/bin/sh -e";
|
||||
shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n echo \"note: skipping skip due to SKIP_ZERO\"\n exit 0\nelif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"note: skipping skip due to ENABLE_PREVIEWS\"\n exit 0\nelif [ \"${ACTION}\" = \"install\" ]; then\n echo \"note: skipping skip due to archive install\"\n exit 0\nelif [ \"${PLATFORM_NAME}\" = \"iphonesimulator\" ]; then\n echo \"note: skipping skip gradle for iOS Simulator\"\n exit 0\nelif [ \"${SKIP_ACTION}\" = \"none\" ]; then\n echo \"note: skipping skip due to SKIP_ACTION none\"\n exit 0\nelse\n SKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Release:${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// swift-tools-version: 6.1
|
||||
// This is a Skip (https://skip.tools) package.
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
@@ -10,15 +9,12 @@ let package = Package(
|
||||
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://source.skip.tools/skip.git", from: "1.8.4"),
|
||||
.package(path: "Vendor/skip-fuse-ui"),
|
||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "PediFoods", dependencies: [
|
||||
.product(name: "SkipFuseUI", package: "skip-fuse-ui"),
|
||||
.product(name: "LCEssentials", package: "lcessentials", condition: .when(platforms: [.iOS]))
|
||||
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
|
||||
.product(name: "LCEssentials", package: "lcessentials")
|
||||
], resources: [.process("Resources")]),
|
||||
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ struct PrimaryButtonLabel: View {
|
||||
.frame(height: 56)
|
||||
.background(AppColors.primary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.labelStyle(.titleOnly)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SnackbarOverlay: View {
|
||||
#if os(Android)
|
||||
let center: SnackbarCenter
|
||||
@State var current: SnackbarMessage?
|
||||
@State var observer: Any?
|
||||
#else
|
||||
@ObservedObject var center: SnackbarCenter
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
#if os(Android)
|
||||
VStack {
|
||||
if let message = current {
|
||||
HStack(spacing: 10) {
|
||||
if let icon = message.iconSystemName, !icon.isEmpty {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
}
|
||||
Text(message.title)
|
||||
.font(AppTypography.heading3)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(3)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
.padding(.top, 14)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(message.style.backgroundColor)
|
||||
.appContentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
center.handleTap()
|
||||
}
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
.zIndex(999)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.9), value: current?.id)
|
||||
.allowsHitTesting(current != nil)
|
||||
.onAppear {
|
||||
current = center.current
|
||||
attachObserverIfNeeded()
|
||||
}
|
||||
.onDisappear {
|
||||
detachObserver()
|
||||
}
|
||||
#else
|
||||
VStack {
|
||||
if let message = center.current {
|
||||
HStack(spacing: 10) {
|
||||
@@ -79,27 +34,5 @@ struct SnackbarOverlay: View {
|
||||
}
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id)
|
||||
.allowsHitTesting(center.current != nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(Android)
|
||||
private func attachObserverIfNeeded() {
|
||||
guard observer == nil else { return }
|
||||
observer = NotificationCenter.default.addObserver(
|
||||
forName: .snackbarDidChange,
|
||||
object: nil,
|
||||
queue: nil
|
||||
) { _ in
|
||||
Task { @MainActor in
|
||||
current = center.current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func detachObserver() {
|
||||
guard let observer else { return }
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
self.observer = nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -95,21 +95,12 @@ struct FeaturedStoreCard: View {
|
||||
|
||||
private var storeIconPlaceholder: some View {
|
||||
ZStack {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("placeholder-product", bundle: .module)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.opacity(0.7)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
#else
|
||||
Image("placeholder-product")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.opacity(0.7)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.clipped()
|
||||
#endif
|
||||
|
||||
Circle()
|
||||
.fill(AppColors.surface.opacity(0.92))
|
||||
@@ -129,6 +120,7 @@ struct FeaturedStoreCardModel: Identifiable {
|
||||
let rating: Double
|
||||
let reviews: String
|
||||
let distance: String
|
||||
let deliveryFee: Double?
|
||||
let category: String
|
||||
let promoText: String?
|
||||
let isFavorite: Bool
|
||||
|
||||
@@ -12,16 +12,10 @@ struct ContentView: View {
|
||||
@State var shouldPulseLaunchSplash = true
|
||||
@State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil
|
||||
@State var authEntryAnimationToken = 0
|
||||
#if os(iOS)
|
||||
@State private var sessionExpiredObserver: NSObjectProtocol?
|
||||
#endif
|
||||
@State var cartResetObserver: Any?
|
||||
@State var appResumeObserver: Any?
|
||||
#if os(Android)
|
||||
@State var snackbarCenter = SnackbarCenter.shared
|
||||
#else
|
||||
@StateObject var snackbarCenter = SnackbarCenter.shared
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
@@ -95,16 +89,12 @@ struct ContentView: View {
|
||||
.onAppear {
|
||||
attachCartResetObserverIfNeeded()
|
||||
attachAppResumeObserverIfNeeded()
|
||||
#if os(iOS)
|
||||
attachSessionExpiredObserverIfNeeded()
|
||||
#endif
|
||||
}
|
||||
.onDisappear {
|
||||
detachCartResetObserver()
|
||||
detachAppResumeObserver()
|
||||
#if os(iOS)
|
||||
detachSessionExpiredObserver()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +392,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private func attachSessionExpiredObserverIfNeeded() {
|
||||
guard sessionExpiredObserver == nil else { return }
|
||||
sessionExpiredObserver = NotificationCenter.default.addObserver(
|
||||
@@ -421,7 +410,6 @@ struct ContentView: View {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
sessionExpiredObserver = nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
struct AddressPickerModalView: View {
|
||||
|
||||
@@ -1,60 +1,30 @@
|
||||
import Foundation
|
||||
import SkipFuse
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
/// A logger for the PediFoods module.
|
||||
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
|
||||
|
||||
/// The shared top-level view for the app, loaded from the platform-specific App delegates below.
|
||||
///
|
||||
/// The default implementation merely loads the `ContentView` for the app and logs a message.
|
||||
/* SKIP @bridge */public struct PediFoodsRootView : View {
|
||||
/* SKIP @bridge */public init() {
|
||||
}
|
||||
public struct PediFoodsRootView: View {
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
ContentView()
|
||||
.task {
|
||||
logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global application delegate functions.
|
||||
///
|
||||
/// These functions can update a shared observable object to communicate app state changes to interested views.
|
||||
/* SKIP @bridge */public final class PediFoodsAppDelegate : Sendable {
|
||||
/* SKIP @bridge */public static let shared = PediFoodsAppDelegate()
|
||||
public final class PediFoodsAppDelegate: Sendable {
|
||||
public static let shared = PediFoodsAppDelegate()
|
||||
|
||||
private init() {
|
||||
}
|
||||
private init() {}
|
||||
|
||||
/* SKIP @bridge */public func onInit() {
|
||||
logger.debug("onInit")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLaunch() {
|
||||
logger.debug("onLaunch")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onResume() {
|
||||
public func onInit() { logger.debug("onInit") }
|
||||
public func onLaunch() { logger.debug("onLaunch") }
|
||||
public func onResume() {
|
||||
logger.debug("onResume")
|
||||
NotificationCenter.default.post(name: .appDidResume, object: nil)
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onPause() {
|
||||
logger.debug("onPause")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onStop() {
|
||||
logger.debug("onStop")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onDestroy() {
|
||||
logger.debug("onDestroy")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLowMemory() {
|
||||
logger.debug("onLowMemory")
|
||||
}
|
||||
public func onPause() { logger.debug("onPause") }
|
||||
public func onStop() { logger.debug("onStop") }
|
||||
public func onDestroy() { logger.debug("onDestroy") }
|
||||
public func onLowMemory() { logger.debug("onLowMemory") }
|
||||
}
|
||||
|
||||
@@ -297,6 +297,9 @@
|
||||
"Cardápio indisponível no momento." : {
|
||||
"comment" : "A message displayed when a store's menu is unavailable.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Carregando cardápio..." : {
|
||||
|
||||
},
|
||||
"Carregando pedido..." : {
|
||||
|
||||
|
||||
@@ -152,4 +152,38 @@ struct ValidateDeliveryAddressResult: Decodable {
|
||||
let deliveryFee: Double?
|
||||
let deliveryTime: String?
|
||||
let sameCity: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deliveryAllowed
|
||||
case delivery_allowed
|
||||
case reasonCode
|
||||
case reason_code
|
||||
case reasonMessage
|
||||
case reason_message
|
||||
case deliveryMode
|
||||
case delivery_mode
|
||||
case distance
|
||||
case deliveryFee
|
||||
case delivery_fee
|
||||
case fee
|
||||
case taxa
|
||||
case deliveryTime
|
||||
case delivery_time
|
||||
case sameCity
|
||||
case same_city
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
deliveryAllowed = (try? c.decode(Bool.self, forKey: .deliveryAllowed))
|
||||
?? (try? c.decode(Bool.self, forKey: .delivery_allowed))
|
||||
reasonCode = ApiService.decodeFlexibleString(from: c, keys: [.reasonCode, .reason_code])
|
||||
reasonMessage = ApiService.decodeFlexibleString(from: c, keys: [.reasonMessage, .reason_message])
|
||||
deliveryMode = ApiService.decodeFlexibleString(from: c, keys: [.deliveryMode, .delivery_mode])
|
||||
distance = ApiService.decodeFlexibleDouble(from: c, keys: [.distance])
|
||||
deliveryFee = ApiService.decodeFlexibleDouble(from: c, keys: [.deliveryFee, .delivery_fee, .fee, .taxa])
|
||||
deliveryTime = ApiService.decodeFlexibleString(from: c, keys: [.deliveryTime, .delivery_time])
|
||||
sameCity = (try? c.decode(Bool.self, forKey: .sameCity))
|
||||
?? (try? c.decode(Bool.self, forKey: .same_city))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ enum ReviewPlatform: String, Encodable {
|
||||
static var current: ReviewPlatform {
|
||||
#if os(iOS)
|
||||
return .ios
|
||||
#elseif os(Android)
|
||||
return .android
|
||||
#else
|
||||
return .web
|
||||
#endif
|
||||
|
||||
@@ -267,11 +267,7 @@ final class FeatureControlService {
|
||||
}
|
||||
|
||||
private func platformName() -> String {
|
||||
#if os(Android)
|
||||
return "android"
|
||||
#else
|
||||
return "ios"
|
||||
#endif
|
||||
}
|
||||
|
||||
private func appVersion() -> String {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Configuration file for https://skip.tools project
|
||||
#
|
||||
# Kotlin dependencies and Gradle build options for this module can be configured here
|
||||
#build:
|
||||
# contents:
|
||||
# - block: 'dependencies'
|
||||
# contents:
|
||||
# - 'implementation("androidx.compose.runtime:runtime")'
|
||||
|
||||
# this is a natively-compiled Skip Fuse module
|
||||
skip:
|
||||
mode: 'native'
|
||||
@@ -5,56 +5,6 @@ extension Notification.Name {
|
||||
static let snackbarDidChange = Notification.Name("snackbarDidChange")
|
||||
}
|
||||
|
||||
#if os(Android)
|
||||
@MainActor
|
||||
final class SnackbarCenter {
|
||||
static let shared = SnackbarCenter()
|
||||
var current: SnackbarMessage?
|
||||
private var dismissTask: Task<Void, Never>?
|
||||
|
||||
func show(
|
||||
title: String,
|
||||
style: SnackbarStyle = .info,
|
||||
icon: String? = nil,
|
||||
duration: TimeInterval = 3.5,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
dismissTask?.cancel()
|
||||
current = SnackbarMessage(
|
||||
title: title,
|
||||
style: style,
|
||||
iconSystemName: icon,
|
||||
duration: duration,
|
||||
action: action
|
||||
)
|
||||
notifyChange()
|
||||
|
||||
dismissTask = Task { [weak self] in
|
||||
let nanos = UInt64(max(0.2, duration) * 1_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: nanos)
|
||||
guard !Task.isCancelled else { return }
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func handleTap() {
|
||||
let action = current?.action
|
||||
dismiss(animated: true)
|
||||
action?()
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool) {
|
||||
dismissTask?.cancel()
|
||||
dismissTask = nil
|
||||
current = nil
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
private func notifyChange() {
|
||||
NotificationCenter.default.post(name: .snackbarDidChange, object: nil)
|
||||
}
|
||||
}
|
||||
#else
|
||||
@MainActor
|
||||
final class SnackbarCenter: ObservableObject {
|
||||
static let shared = SnackbarCenter()
|
||||
@@ -105,7 +55,6 @@ final class SnackbarCenter: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
enum SnackbarStyle: Sendable {
|
||||
case info
|
||||
|
||||
@@ -17,9 +17,7 @@ struct TermsOfUseView: View {
|
||||
}
|
||||
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
||||
.navigationTitle("Termos de Uso")
|
||||
#if os(iOS)
|
||||
.appInlineNavigationTitle()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +38,6 @@ struct PrivacyPolicyView: View {
|
||||
}
|
||||
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
||||
.navigationTitle("Privacidade")
|
||||
#if os(iOS)
|
||||
.appInlineNavigationTitle()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,8 @@ struct LoginEmailView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@ViewBuilder private var logoImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -40,9 +35,9 @@ struct LoginEmailView: View {
|
||||
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email)
|
||||
LoginField(icon: "envelope", placeholder: "seu@email.com", keyboardType: .emailAddress, text: $email)
|
||||
|
||||
LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone)
|
||||
LoginField(icon: "phone", placeholder: "(00) 00000-0000", keyboardType: .numberPad, text: $phone)
|
||||
.onChange(of: phone) { _, newValue in
|
||||
let masked = formatPhoneBR(newValue)
|
||||
if masked != newValue {
|
||||
@@ -78,6 +73,7 @@ struct LoginEmailView: View {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(AppTypography.body)
|
||||
.padding(.top, 8)
|
||||
|
||||
@@ -125,6 +121,7 @@ struct LoginEmailView: View {
|
||||
struct LoginField: View {
|
||||
let icon: String
|
||||
let placeholder: String
|
||||
let keyboardType: UIKeyboardType
|
||||
@Binding var text: String
|
||||
|
||||
var body: some View {
|
||||
@@ -135,6 +132,7 @@ struct LoginField: View {
|
||||
TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { }
|
||||
.appNoAutoCap()
|
||||
.foregroundColor(.black)
|
||||
.keyboardType(keyboardType)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(height: 52)
|
||||
|
||||
@@ -15,23 +15,13 @@ struct LoginView: View {
|
||||
@State var lastAnimatedToken = 0
|
||||
|
||||
@ViewBuilder private var logoImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var pinHeroImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image(colorScheme == .dark ? "pin_image_app_dark" : "pin_image_app", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pin_image_app")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -43,13 +33,6 @@ struct LoginView: View {
|
||||
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
|
||||
.ignoresSafeArea()
|
||||
|
||||
#if os(Android)
|
||||
pinHeroImage
|
||||
.scaledToFill()
|
||||
.frame(height: heroHeight + 80)
|
||||
.offset(y: heroVisible ? -60 : -(heroHeight + 220))
|
||||
.opacity(colorScheme == .dark ? 0.08 : 0.15)
|
||||
#else
|
||||
pinHeroImage
|
||||
.scaledToFill()
|
||||
.frame(height: heroHeight + 80)
|
||||
@@ -61,7 +44,6 @@ struct LoginView: View {
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
#endif
|
||||
|
||||
VStack(spacing: 18) {
|
||||
Spacer().frame(height: logoTopInset)
|
||||
@@ -90,6 +72,7 @@ struct LoginView: View {
|
||||
.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?")
|
||||
@@ -97,6 +80,7 @@ struct LoginView: View {
|
||||
Button("Criar conta") {
|
||||
navigate(.registration)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
.font(AppTypography.body)
|
||||
@@ -108,36 +92,15 @@ struct LoginView: View {
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
#if os(Android)
|
||||
// No Android, rodamos a animação localmente de forma direta
|
||||
applyHiddenStateWithoutAnimation()
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
withAnimation(.easeOut(duration: 0.5)) {
|
||||
heroVisible = true
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||
withAnimation(.easeOut(duration: 0.4)) {
|
||||
textVisible = true
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||
withAnimation(.easeOut(duration: 0.4)) {
|
||||
buttonVisible = true
|
||||
}
|
||||
}
|
||||
#else
|
||||
if shouldPrepareEntryAnimation {
|
||||
applyHiddenStateWithoutAnimation()
|
||||
} else {
|
||||
showFinalStateWithoutAnimation()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.task(id: authEntryAnimationToken) {
|
||||
await runEntryAnimationIfNeeded(for: authEntryAnimationToken)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,19 +25,11 @@ struct OtpView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 74, height: 74)
|
||||
.padding(.top, 140)
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 74, height: 74)
|
||||
.padding(.top, 140)
|
||||
#endif
|
||||
|
||||
Text("Verificação")
|
||||
.font(AppTypography.heading1)
|
||||
@@ -72,6 +64,10 @@ struct OtpView: View {
|
||||
if trimmed != newValue {
|
||||
otp = trimmed
|
||||
}
|
||||
if trimmed.count == 8 && !isLoading {
|
||||
isOtpFocused = false
|
||||
validateOtp()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 204)
|
||||
@@ -92,6 +88,7 @@ struct OtpView: View {
|
||||
Button(resendButtonTitle) {
|
||||
resendOtp()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
.disabled(isResending || !canResend)
|
||||
@@ -101,6 +98,7 @@ struct OtpView: View {
|
||||
Button("Colar código") {
|
||||
pasteOtpFromClipboard()
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
.padding(.top, 22)
|
||||
@@ -124,6 +122,7 @@ struct OtpView: View {
|
||||
.padding(.bottom, 18)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.background(AppColors.backgroundLight)
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
|
||||
@@ -18,13 +18,8 @@ struct RegistrationView: View {
|
||||
private var isFormValid: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && acceptedTerms }
|
||||
|
||||
@ViewBuilder private var logoImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -49,9 +44,9 @@ struct RegistrationView: View {
|
||||
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name)
|
||||
LoginField(icon: "envelope", placeholder: "seu@email.com", text: $email)
|
||||
LoginField(icon: "phone", placeholder: "(00) 00000-0000", text: $phone)
|
||||
LoginField(icon: "person", placeholder: "Ex: Maria Silva", keyboardType: .default, text: $name)
|
||||
LoginField(icon: "envelope", placeholder: "seu@email.com", keyboardType: .emailAddress, text: $email)
|
||||
LoginField(icon: "phone", placeholder: "(00) 00000-0000", keyboardType: .namePhonePad, text: $phone)
|
||||
.onChange(of: phone) { _, newValue in
|
||||
let masked = formatPhoneBR(newValue)
|
||||
if masked != newValue {
|
||||
|
||||
@@ -36,12 +36,7 @@ struct LaunchSplashView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var splashLogo: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,8 @@ struct AddAddressFormView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var logoImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -70,8 +65,8 @@ struct AddAddressFormView: View {
|
||||
.padding(.bottom, 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label)
|
||||
LoginField(icon: "mail", placeholder: "CEP", text: $zipCode)
|
||||
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label)
|
||||
LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode)
|
||||
.onChange(of: zipCode) { _, newValue in
|
||||
let masked = formatZipCodeBR(newValue)
|
||||
if masked != newValue {
|
||||
@@ -99,12 +94,12 @@ struct AddAddressFormView: View {
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.horizontal, 6)
|
||||
}
|
||||
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", text: $address)
|
||||
LoginField(icon: "number", placeholder: "Número", text: $number)
|
||||
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement)
|
||||
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood)
|
||||
LoginField(icon: "building.2", placeholder: "Cidade", text: $city)
|
||||
LoginField(icon: "map", placeholder: "Estado (UF)", text: $state)
|
||||
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address)
|
||||
LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number)
|
||||
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement)
|
||||
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood)
|
||||
LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city)
|
||||
LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state)
|
||||
.onChange(of: state) { _, newValue in
|
||||
let normalized = String(newValue.uppercased().prefix(2))
|
||||
if normalized != newValue {
|
||||
|
||||
@@ -61,6 +61,7 @@ extension HomeView {
|
||||
rating: store.rating ?? 0,
|
||||
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
||||
distance: formatDistance(store.distance),
|
||||
deliveryFee: store.deliveryFee,
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||
|
||||
@@ -101,6 +101,7 @@ struct HomeView: View {
|
||||
storeCategory: store.category,
|
||||
storeRating: store.rating,
|
||||
storeDistance: store.distance,
|
||||
storeDeliveryFee: store.deliveryFee,
|
||||
appState: $appState
|
||||
)
|
||||
} label: {
|
||||
@@ -158,6 +159,7 @@ struct HomeView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
@@ -179,6 +181,7 @@ struct HomeView: View {
|
||||
storeCategory: store.category,
|
||||
storeRating: store.rating,
|
||||
storeDistance: store.distance,
|
||||
storeDeliveryFee: store.deliveryFee,
|
||||
appState: $appState
|
||||
)
|
||||
} label: {
|
||||
|
||||
@@ -49,6 +49,7 @@ struct SearchBar: View {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(height: 52)
|
||||
|
||||
@@ -222,29 +222,15 @@ struct OrderTrackingView: View {
|
||||
|
||||
Group {
|
||||
if hasTrackingImage {
|
||||
#if os(Android)
|
||||
SwiftUI.Image(trackingImageName, bundle: .module)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#else
|
||||
Image(trackingImageName)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#endif
|
||||
} else if hasPlaceholderProductImage {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("placeholder-product", bundle: .module)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#else
|
||||
Image("placeholder-product")
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#endif
|
||||
} else {
|
||||
ZStack {
|
||||
Color.black.opacity(0.08)
|
||||
|
||||
@@ -688,22 +688,17 @@ struct OrderEntryDestinationView: View {
|
||||
func resolveRoute() async {
|
||||
defer { isResolvingRoute = false }
|
||||
|
||||
if routeIntent == .tracking {
|
||||
return
|
||||
}
|
||||
|
||||
let order = await fetchOrderForRouting()
|
||||
guard let order else { return }
|
||||
|
||||
if routeIntent == .details {
|
||||
orderDetails = order
|
||||
return
|
||||
}
|
||||
if shouldOpenOrderDetails(for: order) {
|
||||
orderDetails = order
|
||||
return
|
||||
}
|
||||
guard shouldOpenPaymentScreen(for: order) else { return }
|
||||
|
||||
// For both .tracking and .auto: show payment screen if payment is still pending.
|
||||
// Timeline only shows once payment is confirmed or method is off-app.
|
||||
if shouldOpenPaymentScreen(for: order) {
|
||||
let normalizedMethod = normalizePaymentMethod(order)
|
||||
if normalizedMethod.contains("PIX") {
|
||||
let pixFromPayment = order.payment?.pix
|
||||
@@ -711,14 +706,12 @@ struct OrderEntryDestinationView: View {
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
|
||||
pixContext = PixPaymentContext(
|
||||
id: order.id,
|
||||
orderId: order.id,
|
||||
@@ -731,7 +724,6 @@ struct OrderEntryDestinationView: View {
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||
cardContext = CardPaymentContext(
|
||||
orderId: order.id,
|
||||
@@ -742,6 +734,13 @@ struct OrderEntryDestinationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// For .auto only: route terminal/canceled orders to details instead of timeline.
|
||||
if routeIntent == .auto && shouldOpenOrderDetails(for: order) {
|
||||
orderDetails = order
|
||||
}
|
||||
// .tracking (and .auto fallthrough) → nil states → body renders OrderTrackingView
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func fetchOrderForRouting() async -> PublicOrderResult? {
|
||||
logger.info("OrderEntry fetch route orderId=\(orderId)")
|
||||
|
||||
@@ -81,6 +81,7 @@ struct PizzaFlavorAddonsSheet: View {
|
||||
.appInlineNavigationTitle()
|
||||
.appTopBarTrailingToolbar {
|
||||
Button("Concluir") { dismiss() }
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
#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 {
|
||||
if let imageSource = resolvedProfilePicture {
|
||||
AsyncStoreImage(imageURL: imageSource)
|
||||
.frame(width: 104, height: 104)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
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()
|
||||
|
||||
textFieldSection(
|
||||
title: "Foto (URL/Base64)",
|
||||
placeholder: "https://... ou data:image/jpeg;base64,...",
|
||||
text: $profilePicture
|
||||
)
|
||||
.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 resolvedProfilePicture: String? {
|
||||
let trimmed = profilePicture.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard trimmed.isEmpty == false else { return nil }
|
||||
return ImageSourceResolver.resolve(trimmed)
|
||||
}
|
||||
|
||||
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,9 +1,6 @@
|
||||
#if !os(Android)
|
||||
import SwiftUI
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
import PhotosUI
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@@ -17,8 +14,8 @@ struct UserProfileView: View {
|
||||
@State var profilePicture: String = ""
|
||||
@State var isSaving = false
|
||||
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
@State var selectedPhotoItem: PhotosPickerItem?
|
||||
#if os(iOS)
|
||||
@State private var selectedPhotoItem: PhotosPickerItem?
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
@@ -38,7 +35,7 @@ struct UserProfileView: View {
|
||||
.onAppear {
|
||||
hydrateFromAppState()
|
||||
}
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
.onChange(of: selectedPhotoItem) { _, newItem in
|
||||
Task { await applySelectedPhoto(newItem) }
|
||||
}
|
||||
@@ -63,7 +60,7 @@ struct UserProfileView: View {
|
||||
}
|
||||
|
||||
HStack(spacing: 10) {
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) {
|
||||
Text("Trocar Foto")
|
||||
.font(AppTypography.caption)
|
||||
@@ -244,19 +241,15 @@ struct UserProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
@MainActor
|
||||
private func applySelectedPhoto(_ item: PhotosPickerItem?) async {
|
||||
guard let item else { return }
|
||||
do {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
||||
#if canImport(UIKit)
|
||||
guard let image = UIImage(data: data),
|
||||
let jpegData = image.jpegData(compressionQuality: 0.82) else { return }
|
||||
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
|
||||
#else
|
||||
profilePicture = "data:image/jpeg;base64,\(data.base64EncodedString())"
|
||||
#endif
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Não foi possível carregar a foto selecionada.",
|
||||
@@ -268,4 +261,3 @@ struct UserProfileView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user