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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = "<group>"; };
|
||||||
@@ -66,14 +65,6 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
49AB54462B066A7E007B79B2 /* SkipStone */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
499AB9082B0581F4005E8330 /* plugins */,
|
|
||||||
);
|
|
||||||
name = SkipStone;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
49F90C1F2A52156200F06D93 = {
|
49F90C1F2A52156200F06D93 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -83,7 +74,6 @@
|
|||||||
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
|
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
|
||||||
493609562A6B7EAE00C401E2 /* PediFoods */,
|
493609562A6B7EAE00C401E2 /* PediFoods */,
|
||||||
49F90C2A2A52156200F06D93 /* App */,
|
49F90C2A2A52156200F06D93 /* App */,
|
||||||
49AB54462B066A7E007B79B2 /* SkipStone */,
|
|
||||||
496BDBEC2B89A47800C09264 /* Products */,
|
496BDBEC2B89A47800C09264 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -110,7 +100,6 @@
|
|||||||
499CD43A2AC5B799001AE8D8 /* Sources */,
|
499CD43A2AC5B799001AE8D8 /* Sources */,
|
||||||
499CD43C2AC5B799001AE8D8 /* Frameworks */,
|
499CD43C2AC5B799001AE8D8 /* Frameworks */,
|
||||||
499CD43E2AC5B799001AE8D8 /* Resources */,
|
499CD43E2AC5B799001AE8D8 /* Resources */,
|
||||||
499CD4452AC5B869001AE8D8 /* Run skip gradle */,
|
|
||||||
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
|
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
@@ -173,26 +162,6 @@
|
|||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* 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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// swift-tools-version: 6.1
|
// swift-tools-version: 6.1
|
||||||
// This is a Skip (https://skip.tools) package.
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
@@ -10,15 +9,12 @@ let package = Package(
|
|||||||
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
|
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
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")
|
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(name: "PediFoods", dependencies: [
|
.target(name: "PediFoods", dependencies: [
|
||||||
.product(name: "SkipFuseUI", package: "skip-fuse-ui"),
|
.product(name: "LCEssentials", package: "lcessentials")
|
||||||
.product(name: "LCEssentials", package: "lcessentials", condition: .when(platforms: [.iOS]))
|
], resources: [.process("Resources")]),
|
||||||
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
|
|
||||||
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
|
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ struct PrimaryButtonLabel: View {
|
|||||||
.frame(height: 56)
|
.frame(height: 56)
|
||||||
.background(AppColors.primary)
|
.background(AppColors.primary)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
.labelStyle(.titleOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +1,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SnackbarOverlay: View {
|
struct SnackbarOverlay: View {
|
||||||
#if os(Android)
|
|
||||||
let center: SnackbarCenter
|
|
||||||
@State var current: SnackbarMessage?
|
|
||||||
@State var observer: Any?
|
|
||||||
#else
|
|
||||||
@ObservedObject var center: SnackbarCenter
|
@ObservedObject var center: SnackbarCenter
|
||||||
#endif
|
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
VStack {
|
||||||
if let message = center.current {
|
if let message = center.current {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
@@ -79,27 +34,5 @@ struct SnackbarOverlay: View {
|
|||||||
}
|
}
|
||||||
.animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id)
|
.animation(.spring(response: 0.3, dampingFraction: 0.9), value: center.current?.id)
|
||||||
.allowsHitTesting(center.current != nil)
|
.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 {
|
private var storeIconPlaceholder: some View {
|
||||||
ZStack {
|
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")
|
Image("placeholder-product")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.opacity(0.7)
|
.opacity(0.7)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.clipped()
|
.clipped()
|
||||||
#endif
|
|
||||||
|
|
||||||
Circle()
|
Circle()
|
||||||
.fill(AppColors.surface.opacity(0.92))
|
.fill(AppColors.surface.opacity(0.92))
|
||||||
@@ -129,6 +120,7 @@ struct FeaturedStoreCardModel: Identifiable {
|
|||||||
let rating: Double
|
let rating: Double
|
||||||
let reviews: String
|
let reviews: String
|
||||||
let distance: String
|
let distance: String
|
||||||
|
let deliveryFee: Double?
|
||||||
let category: String
|
let category: String
|
||||||
let promoText: String?
|
let promoText: String?
|
||||||
let isFavorite: Bool
|
let isFavorite: Bool
|
||||||
|
|||||||
@@ -12,16 +12,10 @@ struct ContentView: View {
|
|||||||
@State var shouldPulseLaunchSplash = true
|
@State var shouldPulseLaunchSplash = true
|
||||||
@State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil
|
@State var shouldPrepareAuthEntryAnimation = DefaultTokenStore().jwt == nil
|
||||||
@State var authEntryAnimationToken = 0
|
@State var authEntryAnimationToken = 0
|
||||||
#if os(iOS)
|
|
||||||
@State private var sessionExpiredObserver: NSObjectProtocol?
|
@State private var sessionExpiredObserver: NSObjectProtocol?
|
||||||
#endif
|
|
||||||
@State var cartResetObserver: Any?
|
@State var cartResetObserver: Any?
|
||||||
@State var appResumeObserver: Any?
|
@State var appResumeObserver: Any?
|
||||||
#if os(Android)
|
|
||||||
@State var snackbarCenter = SnackbarCenter.shared
|
|
||||||
#else
|
|
||||||
@StateObject var snackbarCenter = SnackbarCenter.shared
|
@StateObject var snackbarCenter = SnackbarCenter.shared
|
||||||
#endif
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
@@ -95,16 +89,12 @@ struct ContentView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
attachCartResetObserverIfNeeded()
|
attachCartResetObserverIfNeeded()
|
||||||
attachAppResumeObserverIfNeeded()
|
attachAppResumeObserverIfNeeded()
|
||||||
#if os(iOS)
|
|
||||||
attachSessionExpiredObserverIfNeeded()
|
attachSessionExpiredObserverIfNeeded()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
detachCartResetObserver()
|
detachCartResetObserver()
|
||||||
detachAppResumeObserver()
|
detachAppResumeObserver()
|
||||||
#if os(iOS)
|
|
||||||
detachSessionExpiredObserver()
|
detachSessionExpiredObserver()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +392,6 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
private func attachSessionExpiredObserverIfNeeded() {
|
private func attachSessionExpiredObserverIfNeeded() {
|
||||||
guard sessionExpiredObserver == nil else { return }
|
guard sessionExpiredObserver == nil else { return }
|
||||||
sessionExpiredObserver = NotificationCenter.default.addObserver(
|
sessionExpiredObserver = NotificationCenter.default.addObserver(
|
||||||
@@ -421,7 +410,6 @@ struct ContentView: View {
|
|||||||
NotificationCenter.default.removeObserver(observer)
|
NotificationCenter.default.removeObserver(observer)
|
||||||
sessionExpiredObserver = nil
|
sessionExpiredObserver = nil
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AddressPickerModalView: View {
|
struct AddressPickerModalView: View {
|
||||||
|
|||||||
@@ -1,60 +1,30 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SkipFuse
|
import OSLog
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// A logger for the PediFoods module.
|
|
||||||
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
|
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.
|
public struct PediFoodsRootView: View {
|
||||||
///
|
public init() {}
|
||||||
/// 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 var body: some View {
|
public var body: some View {
|
||||||
ContentView()
|
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.
|
public final class PediFoodsAppDelegate: Sendable {
|
||||||
///
|
public static let shared = PediFoodsAppDelegate()
|
||||||
/// 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()
|
|
||||||
|
|
||||||
private init() {
|
private init() {}
|
||||||
}
|
|
||||||
|
|
||||||
/* SKIP @bridge */public func onInit() {
|
public func onInit() { logger.debug("onInit") }
|
||||||
logger.debug("onInit")
|
public func onLaunch() { logger.debug("onLaunch") }
|
||||||
}
|
public func onResume() {
|
||||||
|
|
||||||
/* SKIP @bridge */public func onLaunch() {
|
|
||||||
logger.debug("onLaunch")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SKIP @bridge */public func onResume() {
|
|
||||||
logger.debug("onResume")
|
logger.debug("onResume")
|
||||||
NotificationCenter.default.post(name: .appDidResume, object: nil)
|
NotificationCenter.default.post(name: .appDidResume, object: nil)
|
||||||
}
|
}
|
||||||
|
public func onPause() { logger.debug("onPause") }
|
||||||
/* SKIP @bridge */public func onPause() {
|
public func onStop() { logger.debug("onStop") }
|
||||||
logger.debug("onPause")
|
public func onDestroy() { logger.debug("onDestroy") }
|
||||||
}
|
public func onLowMemory() { logger.debug("onLowMemory") }
|
||||||
|
|
||||||
/* SKIP @bridge */public func onStop() {
|
|
||||||
logger.debug("onStop")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SKIP @bridge */public func onDestroy() {
|
|
||||||
logger.debug("onDestroy")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SKIP @bridge */public func onLowMemory() {
|
|
||||||
logger.debug("onLowMemory")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,9 @@
|
|||||||
"Cardápio indisponível no momento." : {
|
"Cardápio indisponível no momento." : {
|
||||||
"comment" : "A message displayed when a store's menu is unavailable.",
|
"comment" : "A message displayed when a store's menu is unavailable.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Carregando cardápio..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Carregando pedido..." : {
|
"Carregando pedido..." : {
|
||||||
|
|
||||||
|
|||||||
@@ -152,4 +152,38 @@ struct ValidateDeliveryAddressResult: Decodable {
|
|||||||
let deliveryFee: Double?
|
let deliveryFee: Double?
|
||||||
let deliveryTime: String?
|
let deliveryTime: String?
|
||||||
let sameCity: Bool?
|
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 {
|
static var current: ReviewPlatform {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
return .ios
|
return .ios
|
||||||
#elseif os(Android)
|
|
||||||
return .android
|
|
||||||
#else
|
#else
|
||||||
return .web
|
return .web
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -267,11 +267,7 @@ final class FeatureControlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func platformName() -> String {
|
private func platformName() -> String {
|
||||||
#if os(Android)
|
|
||||||
return "android"
|
|
||||||
#else
|
|
||||||
return "ios"
|
return "ios"
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func appVersion() -> String {
|
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")
|
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
|
@MainActor
|
||||||
final class SnackbarCenter: ObservableObject {
|
final class SnackbarCenter: ObservableObject {
|
||||||
static let shared = SnackbarCenter()
|
static let shared = SnackbarCenter()
|
||||||
@@ -105,7 +55,6 @@ final class SnackbarCenter: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
enum SnackbarStyle: Sendable {
|
enum SnackbarStyle: Sendable {
|
||||||
case info
|
case info
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ struct TermsOfUseView: View {
|
|||||||
}
|
}
|
||||||
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
||||||
.navigationTitle("Termos de Uso")
|
.navigationTitle("Termos de Uso")
|
||||||
#if os(iOS)
|
|
||||||
.appInlineNavigationTitle()
|
.appInlineNavigationTitle()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +38,6 @@ struct PrivacyPolicyView: View {
|
|||||||
}
|
}
|
||||||
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
|
||||||
.navigationTitle("Privacidade")
|
.navigationTitle("Privacidade")
|
||||||
#if os(iOS)
|
|
||||||
.appInlineNavigationTitle()
|
.appInlineNavigationTitle()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,8 @@ struct LoginEmailView: View {
|
|||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@ViewBuilder private var logoImage: some View {
|
@ViewBuilder private var logoImage: some View {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("pedifoods", bundle: .module)
|
|
||||||
.resizable()
|
|
||||||
#else
|
|
||||||
SwiftUI.Image("pedifoods")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -40,9 +35,9 @@ struct LoginEmailView: View {
|
|||||||
|
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
VStack(alignment: .leading, 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
|
.onChange(of: phone) { _, newValue in
|
||||||
let masked = formatPhoneBR(newValue)
|
let masked = formatPhoneBR(newValue)
|
||||||
if masked != newValue {
|
if masked != newValue {
|
||||||
@@ -78,6 +73,7 @@ struct LoginEmailView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
|
|
||||||
@@ -125,6 +121,7 @@ struct LoginEmailView: View {
|
|||||||
struct LoginField: View {
|
struct LoginField: View {
|
||||||
let icon: String
|
let icon: String
|
||||||
let placeholder: String
|
let placeholder: String
|
||||||
|
let keyboardType: UIKeyboardType
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -135,6 +132,7 @@ struct LoginField: View {
|
|||||||
TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { }
|
TextField(text: $text, prompt: Text(placeholder).foregroundColor(Color.gray)) { }
|
||||||
.appNoAutoCap()
|
.appNoAutoCap()
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
|
.keyboardType(keyboardType)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.frame(height: 52)
|
.frame(height: 52)
|
||||||
|
|||||||
@@ -15,23 +15,13 @@ struct LoginView: View {
|
|||||||
@State var lastAnimatedToken = 0
|
@State var lastAnimatedToken = 0
|
||||||
|
|
||||||
@ViewBuilder private var logoImage: some View {
|
@ViewBuilder private var logoImage: some View {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("pedifoods", bundle: .module)
|
|
||||||
.resizable()
|
|
||||||
#else
|
|
||||||
SwiftUI.Image("pedifoods")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var pinHeroImage: some View {
|
@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")
|
SwiftUI.Image("pin_image_app")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -43,13 +33,6 @@ struct LoginView: View {
|
|||||||
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
|
(colorScheme == .dark ? Color.black : AppColors.backgroundLight)
|
||||||
.ignoresSafeArea()
|
.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
|
pinHeroImage
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(height: heroHeight + 80)
|
.frame(height: heroHeight + 80)
|
||||||
@@ -61,7 +44,6 @@ struct LoginView: View {
|
|||||||
endPoint: .bottom
|
endPoint: .bottom
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
#endif
|
|
||||||
|
|
||||||
VStack(spacing: 18) {
|
VStack(spacing: 18) {
|
||||||
Spacer().frame(height: logoTopInset)
|
Spacer().frame(height: logoTopInset)
|
||||||
@@ -90,6 +72,7 @@ struct LoginView: View {
|
|||||||
.tint(AppColors.tertiary)
|
.tint(AppColors.tertiary)
|
||||||
.offset(y: buttonVisible ? 0 : 140)
|
.offset(y: buttonVisible ? 0 : 140)
|
||||||
.opacity(buttonVisible ? 1.0 : 0.0)
|
.opacity(buttonVisible ? 1.0 : 0.0)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text("Não tem conta ainda?")
|
Text("Não tem conta ainda?")
|
||||||
@@ -97,6 +80,7 @@ struct LoginView: View {
|
|||||||
Button("Criar conta") {
|
Button("Criar conta") {
|
||||||
navigate(.registration)
|
navigate(.registration)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.foregroundStyle(AppColors.primary)
|
.foregroundStyle(AppColors.primary)
|
||||||
}
|
}
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
@@ -108,36 +92,15 @@ struct LoginView: View {
|
|||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onAppear {
|
.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 {
|
if shouldPrepareEntryAnimation {
|
||||||
applyHiddenStateWithoutAnimation()
|
applyHiddenStateWithoutAnimation()
|
||||||
} else {
|
} else {
|
||||||
showFinalStateWithoutAnimation()
|
showFinalStateWithoutAnimation()
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
|
||||||
.task(id: authEntryAnimationToken) {
|
.task(id: authEntryAnimationToken) {
|
||||||
await runEntryAnimationIfNeeded(for: authEntryAnimationToken)
|
await runEntryAnimationIfNeeded(for: authEntryAnimationToken)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,19 +25,11 @@ struct OtpView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
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")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 74, height: 74)
|
.frame(width: 74, height: 74)
|
||||||
.padding(.top, 140)
|
.padding(.top, 140)
|
||||||
#endif
|
|
||||||
|
|
||||||
Text("Verificação")
|
Text("Verificação")
|
||||||
.font(AppTypography.heading1)
|
.font(AppTypography.heading1)
|
||||||
@@ -72,6 +64,10 @@ struct OtpView: View {
|
|||||||
if trimmed != newValue {
|
if trimmed != newValue {
|
||||||
otp = trimmed
|
otp = trimmed
|
||||||
}
|
}
|
||||||
|
if trimmed.count == 8 && !isLoading {
|
||||||
|
isOtpFocused = false
|
||||||
|
validateOtp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 204)
|
.frame(height: 204)
|
||||||
@@ -92,6 +88,7 @@ struct OtpView: View {
|
|||||||
Button(resendButtonTitle) {
|
Button(resendButtonTitle) {
|
||||||
resendOtp()
|
resendOtp()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.font(AppTypography.heading3)
|
.font(AppTypography.heading3)
|
||||||
.foregroundStyle(AppColors.primary)
|
.foregroundStyle(AppColors.primary)
|
||||||
.disabled(isResending || !canResend)
|
.disabled(isResending || !canResend)
|
||||||
@@ -101,6 +98,7 @@ struct OtpView: View {
|
|||||||
Button("Colar código") {
|
Button("Colar código") {
|
||||||
pasteOtpFromClipboard()
|
pasteOtpFromClipboard()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.font(AppTypography.heading3)
|
.font(AppTypography.heading3)
|
||||||
.foregroundStyle(AppColors.primary)
|
.foregroundStyle(AppColors.primary)
|
||||||
.padding(.top, 22)
|
.padding(.top, 22)
|
||||||
@@ -124,6 +122,7 @@ struct OtpView: View {
|
|||||||
.padding(.bottom, 18)
|
.padding(.bottom, 18)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
.background(AppColors.backgroundLight)
|
.background(AppColors.backgroundLight)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onAppear {
|
.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 }
|
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 {
|
@ViewBuilder private var logoImage: some View {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("pedifoods", bundle: .module)
|
|
||||||
.resizable()
|
|
||||||
#else
|
|
||||||
SwiftUI.Image("pedifoods")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -49,9 +44,9 @@ struct RegistrationView: View {
|
|||||||
|
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
LoginField(icon: "person", placeholder: "Ex: Maria Silva", text: $name)
|
LoginField(icon: "person", placeholder: "Ex: Maria Silva", keyboardType: .default, text: $name)
|
||||||
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: .namePhonePad, text: $phone)
|
||||||
.onChange(of: phone) { _, newValue in
|
.onChange(of: phone) { _, newValue in
|
||||||
let masked = formatPhoneBR(newValue)
|
let masked = formatPhoneBR(newValue)
|
||||||
if masked != newValue {
|
if masked != newValue {
|
||||||
|
|||||||
@@ -36,12 +36,7 @@ struct LaunchSplashView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder var splashLogo: some View {
|
@ViewBuilder var splashLogo: some View {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("pedifoods", bundle: .module)
|
|
||||||
.resizable()
|
|
||||||
#else
|
|
||||||
SwiftUI.Image("pedifoods")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,8 @@ struct AddAddressFormView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var logoImage: some View {
|
@ViewBuilder private var logoImage: some View {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("pedifoods", bundle: .module)
|
|
||||||
.resizable()
|
|
||||||
#else
|
|
||||||
SwiftUI.Image("pedifoods")
|
SwiftUI.Image("pedifoods")
|
||||||
.resizable()
|
.resizable()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -70,8 +65,8 @@ struct AddAddressFormView: View {
|
|||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label)
|
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label)
|
||||||
LoginField(icon: "mail", placeholder: "CEP", text: $zipCode)
|
LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode)
|
||||||
.onChange(of: zipCode) { _, newValue in
|
.onChange(of: zipCode) { _, newValue in
|
||||||
let masked = formatZipCodeBR(newValue)
|
let masked = formatZipCodeBR(newValue)
|
||||||
if masked != newValue {
|
if masked != newValue {
|
||||||
@@ -99,12 +94,12 @@ struct AddAddressFormView: View {
|
|||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
}
|
}
|
||||||
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", text: $address)
|
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address)
|
||||||
LoginField(icon: "number", placeholder: "Número", text: $number)
|
LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number)
|
||||||
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement)
|
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement)
|
||||||
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood)
|
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood)
|
||||||
LoginField(icon: "building.2", placeholder: "Cidade", text: $city)
|
LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city)
|
||||||
LoginField(icon: "map", placeholder: "Estado (UF)", text: $state)
|
LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state)
|
||||||
.onChange(of: state) { _, newValue in
|
.onChange(of: state) { _, newValue in
|
||||||
let normalized = String(newValue.uppercased().prefix(2))
|
let normalized = String(newValue.uppercased().prefix(2))
|
||||||
if normalized != newValue {
|
if normalized != newValue {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ extension HomeView {
|
|||||||
rating: store.rating ?? 0,
|
rating: store.rating ?? 0,
|
||||||
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
||||||
distance: formatDistance(store.distance),
|
distance: formatDistance(store.distance),
|
||||||
|
deliveryFee: store.deliveryFee,
|
||||||
category: store.category ?? "Loja",
|
category: store.category ?? "Loja",
|
||||||
promoText: nil,
|
promoText: nil,
|
||||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ struct HomeView: View {
|
|||||||
storeCategory: store.category,
|
storeCategory: store.category,
|
||||||
storeRating: store.rating,
|
storeRating: store.rating,
|
||||||
storeDistance: store.distance,
|
storeDistance: store.distance,
|
||||||
|
storeDeliveryFee: store.deliveryFee,
|
||||||
appState: $appState
|
appState: $appState
|
||||||
)
|
)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -158,6 +159,7 @@ struct HomeView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.font(AppTypography.heading3)
|
.font(AppTypography.heading3)
|
||||||
.foregroundStyle(AppColors.primary)
|
.foregroundStyle(AppColors.primary)
|
||||||
}
|
}
|
||||||
@@ -179,6 +181,7 @@ struct HomeView: View {
|
|||||||
storeCategory: store.category,
|
storeCategory: store.category,
|
||||||
storeRating: store.rating,
|
storeRating: store.rating,
|
||||||
storeDistance: store.distance,
|
storeDistance: store.distance,
|
||||||
|
storeDeliveryFee: store.deliveryFee,
|
||||||
appState: $appState
|
appState: $appState
|
||||||
)
|
)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ struct SearchBar: View {
|
|||||||
Image(systemName: "slider.horizontal.3")
|
Image(systemName: "slider.horizontal.3")
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.frame(height: 52)
|
.frame(height: 52)
|
||||||
|
|||||||
@@ -222,29 +222,15 @@ struct OrderTrackingView: View {
|
|||||||
|
|
||||||
Group {
|
Group {
|
||||||
if hasTrackingImage {
|
if hasTrackingImage {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image(trackingImageName, bundle: .module)
|
|
||||||
.renderingMode(.original)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
#else
|
|
||||||
Image(trackingImageName)
|
Image(trackingImageName)
|
||||||
.renderingMode(.original)
|
.renderingMode(.original)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
#endif
|
|
||||||
} else if hasPlaceholderProductImage {
|
} else if hasPlaceholderProductImage {
|
||||||
#if os(Android)
|
|
||||||
SwiftUI.Image("placeholder-product", bundle: .module)
|
|
||||||
.renderingMode(.original)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
#else
|
|
||||||
Image("placeholder-product")
|
Image("placeholder-product")
|
||||||
.renderingMode(.original)
|
.renderingMode(.original)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
#endif
|
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.opacity(0.08)
|
Color.black.opacity(0.08)
|
||||||
|
|||||||
@@ -688,22 +688,17 @@ struct OrderEntryDestinationView: View {
|
|||||||
func resolveRoute() async {
|
func resolveRoute() async {
|
||||||
defer { isResolvingRoute = false }
|
defer { isResolvingRoute = false }
|
||||||
|
|
||||||
if routeIntent == .tracking {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let order = await fetchOrderForRouting()
|
let order = await fetchOrderForRouting()
|
||||||
guard let order else { return }
|
guard let order else { return }
|
||||||
|
|
||||||
if routeIntent == .details {
|
if routeIntent == .details {
|
||||||
orderDetails = order
|
orderDetails = order
|
||||||
return
|
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)
|
let normalizedMethod = normalizePaymentMethod(order)
|
||||||
if normalizedMethod.contains("PIX") {
|
if normalizedMethod.contains("PIX") {
|
||||||
let pixFromPayment = order.payment?.pix
|
let pixFromPayment = order.payment?.pix
|
||||||
@@ -711,14 +706,12 @@ struct OrderEntryDestinationView: View {
|
|||||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||||
? pixFromPayment?.copyPaste
|
? pixFromPayment?.copyPaste
|
||||||
: pixFromPayload?.copyPaste
|
: pixFromPayload?.copyPaste
|
||||||
|
|
||||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||||
? pixFromPayment?.qrCodeImage
|
? pixFromPayment?.qrCodeImage
|
||||||
: pixFromPayload?.qrCodeImage
|
: pixFromPayload?.qrCodeImage
|
||||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||||
? pixFromPayment?.expirationDate
|
? pixFromPayment?.expirationDate
|
||||||
: pixFromPayload?.expirationDate
|
: pixFromPayload?.expirationDate
|
||||||
|
|
||||||
pixContext = PixPaymentContext(
|
pixContext = PixPaymentContext(
|
||||||
id: order.id,
|
id: order.id,
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
@@ -731,7 +724,6 @@ struct OrderEntryDestinationView: View {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||||
cardContext = CardPaymentContext(
|
cardContext = CardPaymentContext(
|
||||||
orderId: order.id,
|
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
|
@MainActor
|
||||||
func fetchOrderForRouting() async -> PublicOrderResult? {
|
func fetchOrderForRouting() async -> PublicOrderResult? {
|
||||||
logger.info("OrderEntry fetch route orderId=\(orderId)")
|
logger.info("OrderEntry fetch route orderId=\(orderId)")
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ struct PizzaFlavorAddonsSheet: View {
|
|||||||
.appInlineNavigationTitle()
|
.appInlineNavigationTitle()
|
||||||
.appTopBarTrailingToolbar {
|
.appTopBarTrailingToolbar {
|
||||||
Button("Concluir") { dismiss() }
|
Button("Concluir") { dismiss() }
|
||||||
|
.buttonStyle(.plain)
|
||||||
.foregroundStyle(AppColors.primary)
|
.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
|
import SwiftUI
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if os(iOS)
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
#endif
|
|
||||||
#if canImport(UIKit)
|
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -17,8 +14,8 @@ struct UserProfileView: View {
|
|||||||
@State var profilePicture: String = ""
|
@State var profilePicture: String = ""
|
||||||
@State var isSaving = false
|
@State var isSaving = false
|
||||||
|
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if os(iOS)
|
||||||
@State var selectedPhotoItem: PhotosPickerItem?
|
@State private var selectedPhotoItem: PhotosPickerItem?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -38,7 +35,7 @@ struct UserProfileView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
hydrateFromAppState()
|
hydrateFromAppState()
|
||||||
}
|
}
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if os(iOS)
|
||||||
.onChange(of: selectedPhotoItem) { _, newItem in
|
.onChange(of: selectedPhotoItem) { _, newItem in
|
||||||
Task { await applySelectedPhoto(newItem) }
|
Task { await applySelectedPhoto(newItem) }
|
||||||
}
|
}
|
||||||
@@ -63,7 +60,7 @@ struct UserProfileView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if os(iOS)
|
||||||
PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) {
|
PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) {
|
||||||
Text("Trocar Foto")
|
Text("Trocar Foto")
|
||||||
.font(AppTypography.caption)
|
.font(AppTypography.caption)
|
||||||
@@ -244,19 +241,15 @@ struct UserProfileView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(PhotosUI) && os(iOS)
|
#if os(iOS)
|
||||||
@MainActor
|
@MainActor
|
||||||
private func applySelectedPhoto(_ item: PhotosPickerItem?) async {
|
private func applySelectedPhoto(_ item: PhotosPickerItem?) async {
|
||||||
guard let item else { return }
|
guard let item else { return }
|
||||||
do {
|
do {
|
||||||
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
||||||
#if canImport(UIKit)
|
|
||||||
guard let image = UIImage(data: data),
|
guard let image = UIImage(data: data),
|
||||||
let jpegData = image.jpegData(compressionQuality: 0.82) else { return }
|
let jpegData = image.jpegData(compressionQuality: 0.82) else { return }
|
||||||
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
|
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
|
||||||
#else
|
|
||||||
profilePicture = "data:image/jpeg;base64,\(data.base64EncodedString())"
|
|
||||||
#endif
|
|
||||||
} catch {
|
} catch {
|
||||||
SnackbarCenter.shared.show(
|
SnackbarCenter.shared.show(
|
||||||
title: "Não foi possível carregar a foto selecionada.",
|
title: "Não foi possível carregar a foto selecionada.",
|
||||||
@@ -268,4 +261,3 @@ struct UserProfileView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
Reference in New Issue
Block a user