migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- name: Write App Store Connect API key
run: |
cat > Darwin/fastlane/apikey.json <<EOF
cat > fastlane/apikey.json <<EOF
{
"key_id": "${{ secrets.ASC_KEY_ID }}",
"issuer_id": "${{ secrets.ASC_ISSUER_ID }}",
@@ -52,12 +52,11 @@ jobs:
security list-keychains -d user -s ~/Library/Keychains/ci-signing.keychain-db ~/Library/Keychains/login.keychain-db
security default-keychain -d user -s ~/Library/Keychains/ci-signing.keychain-db
security find-identity -v -p codesigning
cd Darwin
fastlane beta
- name: Clean up API key
if: always()
run: rm -f Darwin/fastlane/apikey.json
run: rm -f fastlane/apikey.json
sleep-macos-vm:
needs: build-and-upload

View File

@@ -42,7 +42,7 @@ jobs:
- name: Write App Store Connect API key
run: |
cat > Darwin/fastlane/apikey.json <<EOF
cat > fastlane/apikey.json <<EOF
{
"key_id": "${{ secrets.ASC_KEY_ID }}",
"issuer_id": "${{ secrets.ASC_ISSUER_ID }}",
@@ -70,12 +70,11 @@ jobs:
security dump-keychain "$HOME/Library/Keychains/ci-signing.keychain-db" | grep -c "class.*0x80001000" || true
echo "--- find-identity ---"
security find-identity -v -p codesigning
cd Darwin
fastlane release
- name: Clean up API key
if: always()
run: rm -f Darwin/fastlane/apikey.json
run: rm -f fastlane/apikey.json
sleep-macos-vm:
needs: build-archive-deliver

View File

@@ -30,8 +30,8 @@ jobs:
- name: Run unit tests
run: |
xcodebuild test \
-project Darwin/PediFoods.xcodeproj \
-scheme "PediFoods App" \
-project PediFoods.xcodeproj \
-scheme "PediFoods" \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest"
sleep-macos-vm:

8
.gitignore vendored
View File

@@ -7,14 +7,6 @@
# macOS
.DS_Store
# gradle properties
local.properties
.gradle/
.android/
.kotlin/
Android/app/keystore.jks
Android/app/keystore.properties
xcodebuild*.log
default.profraw

View File

@@ -1,59 +0,0 @@
#include "../Skip.env"
// Set the action that will be executed as part of the Xcode Run Script phase
// Setting to "launch" will build and run the app in the first open Android emulator or device
// Setting to "build" will just run gradle build, but will not launch the app
// Setting to "none" will completely disable the build and launch of the Android app
SKIP_ACTION = launch
//SKIP_ACTION = build
//SKIP_ACTION = none
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
INFOPLIST_FILE = Info.plist
GENERATE_INFOPLIST_FILE = YES
// The user-visible name of the app (localizable)
//INFOPLIST_KEY_CFBundleDisplayName = App Name
//INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.utilities
//INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This app uses your location to …"
// iOS-specific Info.plist property keys
INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*] = YES
INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = NO
INFOPLIST_KEY_UILaunchStoryboardName[sdk=iphone*] = LaunchScreen
INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault
INFOPLIST_KEY_UISupportedInterfaceOrientations[sdk=iphone*] = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown
IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MACCATALYST = NO
// iPhone + iPad
TARGETED_DEVICE_FAMILY = 1,2
// iPhone only
// TARGETED_DEVICE_FAMILY = 1
SWIFT_EMIT_LOC_STRINGS = YES
// the name of the product module; this can be anything, but cannot conflict with any Swift module names
PRODUCT_MODULE_NAME = $(PRODUCT_NAME:c99extidentifier)App
// On-device testing may need to override the bundle ID
// PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=iphoneos*] = cool.beans.BundleIdentifer
SDKROOT = auto
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_EMIT_LOC_STRINGS = YES
SWIFT_VERSION = 6
// Development team ID for on-device testing
CODE_SIGNING_REQUIRED = NO
CODE_SIGN_STYLE = Automatic
CODE_SIGN_ENTITLEMENTS = Entitlements.plist
//CODE_SIGNING_IDENTITY = -
//DEVELOPMENT_TEAM =

View File

@@ -1,484 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
002E82AE37F71856D2AD0524 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 588B094973FB7448596F3D09 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeaderOnCopy, ); }; };
1457598D2491B8729EEBB739 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E34FD2046E40CE1E302DB11 /* NotificationService.swift */; };
491F27822DA55B72004926EE /* PediFoods in Frameworks */ = {isa = PBXBuildFile; productRef = 491F27812DA55B72004926EE /* PediFoods */; };
491F27832DA55B72004926EE /* PediFoods in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 491F27812DA55B72004926EE /* PediFoods */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */; };
499CD43B2AC5B799001AE8D8 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F90C2B2A52156200F06D93 /* Main.swift */; };
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49F90C2F2A52156300F06D93 /* Assets.xcassets */; };
837DB5EB5FA4527A71F02C70 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78EA2CD32C41A0B833E06B89 /* Foundation.framework */; };
B13CABF22F36437300469FD6 /* LCEssentials in Frameworks */ = {isa = PBXBuildFile; productRef = B13CABF12F36437300469FD6 /* LCEssentials */; };
B185A1FD2FB67A12001C7771 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B185A1FC2FB67A12001C7771 /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
E60A9FE80EBD27A663AF17E0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 49F90C202A52156200F06D93 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4FB6BE053007051C3D26B06E;
remoteInfo = NotificationServiceExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
491F27832DA55B72004926EE /* PediFoods in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
C28D30C677D0B233FE310699 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
002E82AE37F71856D2AD0524 /* NotificationServiceExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4900101C2BACEA710000DE33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
493609562A6B7EAE00C401E2 /* PediFoods */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PediFoods; path = ..; sourceTree = "<group>"; };
496BDBEB2B89A47800C09264 /* PediFoods.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PediFoods.app; sourceTree = BUILT_PRODUCTS_DIR; };
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = ../Sources/PediFoods/Resources/Localizable.xcstrings; 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>"; };
496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
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>"; };
4E34FD2046E40CE1E302DB11 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
588B094973FB7448596F3D09 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
78EA2CD32C41A0B833E06B89 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
B185A1FC2FB67A12001C7771 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Sources/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; };
ED678A4BFF090312B354648E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
499CD43C2AC5B799001AE8D8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B13CABF22F36437300469FD6 /* LCEssentials in Frameworks */,
491F27822DA55B72004926EE /* PediFoods in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CAFA309ED97AD448069C6F95 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
837DB5EB5FA4527A71F02C70 /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
39B295835451899F1689190A /* NotificationServiceExtension */ = {
isa = PBXGroup;
children = (
4E34FD2046E40CE1E302DB11 /* NotificationService.swift */,
ED678A4BFF090312B354648E /* Info.plist */,
);
name = NotificationServiceExtension;
path = NotificationServiceExtension;
sourceTree = "<group>";
};
496BDBEC2B89A47800C09264 /* Products */ = {
isa = PBXGroup;
children = (
496BDBEB2B89A47800C09264 /* PediFoods.app */,
588B094973FB7448596F3D09 /* NotificationServiceExtension.appex */,
);
name = Products;
sourceTree = "<group>";
};
49F90C1F2A52156200F06D93 = {
isa = PBXGroup;
children = (
496EB72F2A6AE4DE00C1253C /* README.md */,
496EB72F2A6AE4DE00C1253A /* Skip.env */,
496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */,
496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */,
493609562A6B7EAE00C401E2 /* PediFoods */,
49F90C2A2A52156200F06D93 /* App */,
496BDBEC2B89A47800C09264 /* Products */,
39B295835451899F1689190A /* NotificationServiceExtension */,
8F798D6B741A681CBA9CC824 /* Frameworks */,
);
sourceTree = "<group>";
};
49F90C2A2A52156200F06D93 /* App */ = {
isa = PBXGroup;
children = (
49F90C2B2A52156200F06D93 /* Main.swift */,
B185A1FC2FB67A12001C7771 /* LaunchScreen.storyboard */,
49F90C2F2A52156300F06D93 /* Assets.xcassets */,
49F90C312A52156300F06D93 /* Entitlements.plist */,
4900101C2BACEA710000DE33 /* Info.plist */,
);
name = App;
sourceTree = "<group>";
};
8F798D6B741A681CBA9CC824 /* Frameworks */ = {
isa = PBXGroup;
children = (
C127D57D66FAAE472B202C61 /* iOS */,
);
name = Frameworks;
sourceTree = "<group>";
};
C127D57D66FAAE472B202C61 /* iOS */ = {
isa = PBXGroup;
children = (
78EA2CD32C41A0B833E06B89 /* Foundation.framework */,
);
name = iOS;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
499CD4382AC5B799001AE8D8 /* PediFoods App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "PediFoods App" */;
buildPhases = (
499CD43A2AC5B799001AE8D8 /* Sources */,
499CD43C2AC5B799001AE8D8 /* Frameworks */,
499CD43E2AC5B799001AE8D8 /* Resources */,
499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */,
C28D30C677D0B233FE310699 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
A3838B46B6FB8B79EE9B0F9F /* PBXTargetDependency */,
);
name = "PediFoods App";
packageProductDependencies = (
491F27812DA55B72004926EE /* PediFoods */,
B13CABF12F36437300469FD6 /* LCEssentials */,
);
productName = App;
productReference = 496BDBEB2B89A47800C09264 /* PediFoods.app */;
productType = "com.apple.product-type.application";
};
4FB6BE053007051C3D26B06E /* NotificationServiceExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = A20C22A5541093DB9CFD38F4 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */;
buildPhases = (
29CCE086EDBB10F49FF72DDA /* Sources */,
CAFA309ED97AD448069C6F95 /* Frameworks */,
7A8A5D1C5EC61B8ACACF33CF /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = NotificationServiceExtension;
productName = NotificationServiceExtension;
productReference = 588B094973FB7448596F3D09 /* NotificationServiceExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
49F90C202A52156200F06D93 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1630;
TargetAttributes = {
499CD4382AC5B799001AE8D8 = {
SystemCapabilities = {
com.apple.Push = {
enabled = 1;
};
};
};
};
};
buildConfigurationList = 49F90C232A52156200F06D93 /* Build configuration list for PBXProject "PediFoods" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
es,
ja,
"zh-Hans",
);
mainGroup = 49F90C1F2A52156200F06D93;
packageReferences = (
B13CABF02F36437300469FD6 /* XCRemoteSwiftPackageReference "LCEssentials" */,
);
productRefGroup = 496BDBEC2B89A47800C09264 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
499CD4382AC5B799001AE8D8 /* PediFoods App */,
4FB6BE053007051C3D26B06E /* NotificationServiceExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
499CD43E2AC5B799001AE8D8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B185A1FD2FB67A12001C7771 /* LaunchScreen.storyboard in Resources */,
499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */,
496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
7A8A5D1C5EC61B8ACACF33CF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
29CCE086EDBB10F49FF72DDA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1457598D2491B8729EEBB739 /* NotificationService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
499CD43A2AC5B799001AE8D8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
499CD43B2AC5B799001AE8D8 /* Main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
A3838B46B6FB8B79EE9B0F9F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = NotificationServiceExtension;
target = 4FB6BE053007051C3D26B06E /* NotificationServiceExtension */;
targetProxy = E60A9FE80EBD27A663AF17E0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
499CD4422AC5B799001AE8D8 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Entitlements.plist;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods uses your location to show nearby stores and estimate delivery times.";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "PediFoods Dev Prov Prof";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
499CD4432AC5B799001AE8D8 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Entitlements.plist;
CODE_SIGN_IDENTITY = "Apple Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = K4E5BZMM4V;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = K4E5BZMM4V;
ENABLE_PREVIEWS = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "PediFoods uses your location to show nearby stores and estimate delivery times.";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
PROVISIONING_PROFILE_SPECIFIER = "com.br.pedifoods.app AppStore";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "PediFoods Dist XC Prov Prof";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
49F90C4B2A52156300F06D93 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
49F90C4C2A52156300F06D93 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
602E0FA08B93FC360B6AF5D1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K4E5BZMM4V;
INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.br.pedifoods.app.NotificationService;
PRODUCT_NAME = NotificationService;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 6;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
F90A5B614A60B25B97F3541D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K4E5BZMM4V;
INFOPLIST_FILE = NotificationServiceExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.br.pedifoods.app.NotificationService;
PRODUCT_NAME = NotificationService;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 6;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "PediFoods App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
499CD4422AC5B799001AE8D8 /* Debug */,
499CD4432AC5B799001AE8D8 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
49F90C232A52156200F06D93 /* Build configuration list for PBXProject "PediFoods" */ = {
isa = XCConfigurationList;
buildConfigurations = (
49F90C4B2A52156300F06D93 /* Debug */,
49F90C4C2A52156300F06D93 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A20C22A5541093DB9CFD38F4 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F90A5B614A60B25B97F3541D /* Release */,
602E0FA08B93FC360B6AF5D1 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
B13CABF02F36437300469FD6 /* XCRemoteSwiftPackageReference "LCEssentials" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.11;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
491F27812DA55B72004926EE /* PediFoods */ = {
isa = XCSwiftPackageProductDependency;
productName = PediFoods;
};
B13CABF12F36437300469FD6 /* LCEssentials */ = {
isa = XCSwiftPackageProductDependency;
package = B13CABF02F36437300469FD6 /* XCRemoteSwiftPackageReference "LCEssentials" */;
productName = LCEssentials;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 49F90C202A52156200F06D93 /* Project object */;
}

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "499CD4382AC5B799001AE8D8"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods App"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,86 +0,0 @@
import SwiftUI
import PediFoods
private typealias AppRootView = PediFoodsRootView
private typealias AppDelegate = PediFoodsAppDelegate
/// The entry point to the app simply loads the App implementation from SPM module.
@main struct AppMain: App {
@AppDelegateAdaptor(AppMainDelegate.self) var appDelegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
AppRootView()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
AppDelegate.shared.onResume()
case .inactive:
AppDelegate.shared.onPause()
case .background:
AppDelegate.shared.onStop()
@unknown default:
print("unknown app phase: \(newPhase)")
}
}
}
}
#if canImport(UIKit)
typealias AppDelegateAdaptor = UIApplicationDelegateAdaptor
typealias AppMainDelegateBase = UIApplicationDelegate
typealias AppType = UIApplication
#elseif canImport(AppKit)
typealias AppDelegateAdaptor = NSApplicationDelegateAdaptor
typealias AppMainDelegateBase = NSApplicationDelegate
typealias AppType = NSApplication
#endif
@MainActor final class AppMainDelegate: NSObject, AppMainDelegateBase {
let application = AppType.shared
#if canImport(UIKit)
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.shared.onInit()
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.shared.onLaunch()
return true
}
func applicationWillTerminate(_ application: UIApplication) {
AppDelegate.shared.onDestroy()
}
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
AppDelegate.shared.onLowMemory()
}
// support for SkipNotify.fetchNotificationToken()
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: NSNotification.Name("didRegisterForRemoteNotificationsWithDeviceToken"), object: application, userInfo: ["deviceToken": deviceToken])
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
NotificationCenter.default.post(name: NSNotification.Name("didFailToRegisterForRemoteNotificationsWithError"), object: application, userInfo: ["error": error])
}
#elseif canImport(AppKit)
func applicationWillFinishLaunching(_ notification: Notification) {
AppDelegate.shared.onInit()
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppDelegate.shared.onLaunch()
}
func applicationWillTerminate(_ application: Notification) {
AppDelegate.shared.onDestroy()
}
#endif
}

86
LICENSE.md Normal file
View File

@@ -0,0 +1,86 @@
![](loverde_company_logo_full.png)
Copyright (C) Loverde Company - All Rights Reserved
----------
# ⚠️ INFORMAÇÕES CONFIDENCIAIS E PROPRIEDADE INTELECTUAL ⚠️
## 🚨 AVISO LEGAL IMPORTANTE
**ATENÇÃO: Este repositório contém informações CONFIDENCIAIS e PROPRIETÁRIAS.**
### ⛔ PROIBIÇÕES ABSOLUTAS
- **NÃO** compartilhe este código com terceiros
- **NÃO** faça fork, clone ou cópia sem autorização expressa
- **NÃO** utilize qualquer parte deste código em outros projetos
- **NÃO** distribua, venda ou comercialize este software
- **NÃO** reverse engineer ou descompile o código
- **NÃO** acesse sem permissão explícita do proprietário
### 🔒 DIREITOS DE PROPRIEDADE INTELECTUAL
Este software é propriedade exclusiva de **Daniel Arantes Loverde** e está protegido por:
- **Lei de Propriedade Intelectual (Lei 9.610/98)**
- **Lei de Software (Lei 9.609/98)**
- **Lei Geral de Proteção de Dados (LGPD - Lei 13.709/18)**
- **Direitos Autorais e Patentes**
### ⚖️ CONSEQUÊNCIAS LEGAIS
O acesso não autorizado, uso indevido, cópia ou distribuição deste código constitui:
- **CRIME** de violação de propriedade intelectual
- **CRIME** de roubo de propriedade intelectual
- **CRIME** de violação de direitos autorais
- **CRIME** de acesso não autorizado a sistema informatizado
**Penas previstas: 2 a 5 anos de reclusão + multa**
### 🛡️ MEDIDAS DE SEGURANÇA
- **Monitoramento**: Todo acesso é registrado e monitorado
- **Rastreamento**: IPs e atividades são rastreados
- **Logs**: Sistema completo de auditoria
- **Criptografia**: Dados protegidos com AES-256
- **Autenticação**: Acesso restrito com JWT + OTP
### 📧 CONTATO AUTORIZADO
**ÚNICO CONTATO VÁLIDO:**
- **Email**: daniel@loverde.com.br
- **Autorização**: Apenas via email oficial
### 🔐 CLASSIFICAÇÃO
**NÍVEL DE CONFIDENCIALIDADE: MÁXIMO**
- **Restrito**: Apenas para o proprietário
- **Não Divulgar**: Informações proprietárias
- **Não Compartilhar**: Código fonte protegido
---
## ⚠️ DISCLAIMER FINAL
**Ao acessar este repositório, você concorda que:**
1. **Reconhece** que este é um software proprietário
2. **Compromete-se** a não violar os direitos de propriedade intelectual
3. **Aceita** as consequências legais por qualquer violação
4. **Entende** que o acesso é monitorado e rastreado
5. **Concorda** em respeitar todas as leis aplicáveis
---
*Este documento é parte integrante do software e deve ser mantido confidencial.*
---
This source code is protected under international copyright law. All rights
reserved and protected by the copyright holders.
This file is confidential and only available to authorized individuals with the
permission of the copyright holders. If you encounter this file and do not have
permission, please contact the copyright holders and delete this file.
Autor: Daniel Arantes Loverde

View File

@@ -1,31 +0,0 @@
e# Plano - Tela de Perfil
Legenda:
- 🟢 Concluído
- 🟡 Em validação
- 🔴 Pendente
Última atualização: 2026-02-13
## Entregas
- 🟢 Redesign do conteúdo da tela de perfil no estilo do modelo (header + cards de menu)
- 🟢 Header ajustada para encostar no topo e nas laterais
- 🟢 Redução de escala dos itens da lista (ícones, fonte, altura e paddings)
- 🟢 Faixa de proteção no rodapé para não deixar conteúdo por trás da tabbar
- 🟢 Confirmação com alert padrão ao tocar em "Sair da Conta"
- 🟢 Fluxo de editar endereço reutilizando a tela de novo endereço
- 🟢 Swipe para excluir endereço com compatibilidade Android (gesto custom)
- 🟢 Polimento visual do swipe/delete (sem vazamento de fundo vermelho)
## Pendências
- 🟡 Validação visual final no device (iOS e Android) com screenshots finais
- 🔴 Definir destinos reais dos itens ainda placeholder:
- Meus Cartões
- Minhas Avaliações
- Cupons de Desconto
- Configurações/Ajuda (confirmar nomenclatura final)
- 🔴 Corrigir build global do projeto (dependência `LCEssentials` ausente em `StoreDetailView.swift`)
Autor: Daniel Arantes Loverde

View File

@@ -1,52 +0,0 @@
# Análise Técnica — Arquitetura Skip (PediFoods)
## Entendimento confirmado
O projeto usa **Skip com fonte principal em Swift/SwiftUI** e geração/transpilação para Android.
Diretriz operacional validada:
1. **iOS é a plataforma prioritária** de desenvolvimento e validação.
2. **Android entra depois**, com adaptações isoladas por compilação condicional.
3. Toda alteração deve preservar comportamento de iOS e evitar regressão cross-platform.
## Como o projeto está estruturado
1. Código compartilhado principal: `Sources/PediFoods/*` (Swift/SwiftUI).
2. Entry point iOS: `Darwin/Sources/Main.swift`.
3. Entry point Android: `Android/app/src/main/kotlin/Main.kt`.
4. Configuração Skip: `Sources/PediFoods/Skip/skip.yml` (`mode: native`).
5. Build/transpilação Android via plugin Skip (`skipstone`) no target Swift Package.
## Evidências no código (padrão já em uso)
1. Uso extensivo de condicionais:
- `#if os(iOS)`
- `#if os(Android)`
- `#if canImport(UIKit)`
2. Dependência iOS-only no `Package.swift`:
- `LCEssentials` com `condition: .when(platforms: [.iOS])`
3. Exemplo claro de adaptação segura:
- `ContentView.swift`: `SnackbarCenter` com implementação distinta para Android e iOS.
## Regra prática para próximas tarefas
1. Implementar primeiro para iOS.
2. Validar iOS (build/execução/comportamento).
3. Só depois adaptar Android, sempre em bloco condicional separado.
4. Nunca alterar caminho iOS por causa de ajuste Android.
5. Em mudanças de UI de inicialização (ex.: splash), tratar iOS e Android em fluxos independentes.
## Nota específica para Splash
Estado atual identificado:
1. iOS com Launch Screen gerado automaticamente (`INFOPLIST_KEY_UILaunchScreen_Generation = YES`).
2. Android sem implementação custom de splash dedicada no `res/values` + tema de launch.
Implicação:
1. Mudança de splash para iOS pode ser feita sem tocar Android.
2. Quando Android for tratado, deve entrar em camada própria (tema/recursos Android), sem impactar o fluxo iOS.
Autor: Daniel Arantes Loverde

View File

@@ -1,20 +0,0 @@
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "pedi-foods",
defaultLocalization: "en",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "PediFoods", type: .dynamic, targets: ["PediFoods"]),
],
dependencies: [
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git", from: "1.0.5")
],
targets: [
.target(name: "PediFoods", dependencies: [
.product(name: "LCEssentials", package: "lcessentials")
], resources: [.process("Resources")]),
.testTarget(name: "PediFoodsTests", dependencies: ["PediFoods"]),
]
)

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "group:Darwin/PediFoods.xcodeproj">
location = "self:">
</FileRef>
</Workspace>
</Workspace>

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B3B5CFF77A8ED019D1299ABB"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3027BAE353B414EC70E2A2AC"
BuildableName = "PediFoodsTests.xctest"
BlueprintName = "PediFoodsTests"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66486D25E3A8A75EA8FACE44"
BuildableName = "PediFoodsUITests.xctest"
BlueprintName = "PediFoodsUITests"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B3B5CFF77A8ED019D1299ABB"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3027BAE353B414EC70E2A2AC"
BuildableName = "PediFoodsTests.xctest"
BlueprintName = "PediFoodsTests"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "66486D25E3A8A75EA8FACE44"
BuildableName = "PediFoodsUITests.xctest"
BlueprintName = "PediFoodsUITests"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B3B5CFF77A8ED019D1299ABB"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B3B5CFF77A8ED019D1299ABB"
BuildableName = "PediFoods.app"
BlueprintName = "PediFoods"
ReferencedContainer = "container:PediFoods.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 835 B

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 667 KiB

After

Width:  |  Height:  |  Size: 667 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

Before

Width:  |  Height:  |  Size: 924 KiB

After

Width:  |  Height:  |  Size: 924 KiB

View File

Before

Width:  |  Height:  |  Size: 951 KiB

After

Width:  |  Height:  |  Size: 951 KiB

View File

Before

Width:  |  Height:  |  Size: 940 KiB

After

Width:  |  Height:  |  Size: 940 KiB

View File

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 926 KiB

View File

Before

Width:  |  Height:  |  Size: 799 KiB

After

Width:  |  Height:  |  Size: 799 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,10 +1,6 @@
import Foundation
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
enum ImageFitMode {
/// Scale to cover the whole box, cropping whichever axis overflows.
@@ -22,9 +18,7 @@ struct CachedRemoteImage<Placeholder: View>: View {
let fitMode: ImageFitMode
let placeholder: Placeholder
#if canImport(UIKit) || canImport(AppKit)
@StateObject var loader = CachedRemoteImageLoader()
#endif
init(
imageURL: String?,
@@ -41,52 +35,23 @@ struct CachedRemoteImage<Placeholder: View>: View {
var body: some View {
GeometryReader { geometry in
Group {
#if canImport(UIKit)
if let uiImage = loader.uiImage {
rendered(image: Image(uiImage: uiImage), pixelSize: uiImage.size, in: geometry.size)
} else {
placeholder
}
#elseif canImport(AppKit)
if let nsImage = loader.nsImage {
rendered(image: Image(nsImage: nsImage), pixelSize: nsImage.size, in: geometry.size)
} else {
placeholder
}
#else
if let imageURL,
let url = URL(string: imageURL) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFill()
default:
placeholder
}
}
} else {
placeholder
}
#endif
}
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
.onAppear {
#if canImport(UIKit) || canImport(AppKit)
loader.load(imageURL: imageURL, ttl: ttl)
#endif
}
.onChange(of: imageURL) { _, newValue in
#if canImport(UIKit) || canImport(AppKit)
loader.load(imageURL: newValue, ttl: ttl)
#endif
}
}
#if canImport(UIKit) || canImport(AppKit)
@ViewBuilder
private func rendered(image: Image, pixelSize: CGSize, in containerSize: CGSize) -> some View {
switch fitMode {
@@ -103,17 +68,11 @@ struct CachedRemoteImage<Placeholder: View>: View {
.frame(width: containerSize.width, height: containerSize.height)
}
}
#endif
}
#if canImport(UIKit) || canImport(AppKit)
@MainActor
final class CachedRemoteImageLoader: ObservableObject {
#if canImport(UIKit)
@Published var uiImage: UIImage?
#elseif canImport(AppKit)
@Published var nsImage: NSImage?
#endif
private var currentKey: String?
private var task: Task<Void, Never>?
@@ -130,50 +89,30 @@ final class CachedRemoteImageLoader: ObservableObject {
task?.cancel()
#if canImport(UIKit)
uiImage = nil
#elseif canImport(AppKit)
nsImage = nil
#endif
guard let normalized, normalized.isEmpty == false else { return }
#if canImport(UIKit) || canImport(AppKit)
let dataCacheKey = Self.dataURLCacheKey(normalized)
if let cachedDataImage: PlatformImage = AppContentCache.shared.value(for: dataCacheKey, as: PlatformImage.self) {
#if canImport(UIKit)
uiImage = cachedDataImage
#elseif canImport(AppKit)
nsImage = cachedDataImage
#endif
return
}
if let image = Self.imageFromDataURL(normalized) {
AppContentCache.shared.set(image, for: dataCacheKey, ttl: ttl)
#if canImport(UIKit)
uiImage = image
#elseif canImport(AppKit)
nsImage = image
#endif
return
}
#endif
guard let url = URL(string: normalized) else { return }
task = Task { [weak self] in
#if canImport(UIKit) || canImport(AppKit)
let image = await AppImageCache.shared.image(for: url, ttl: ttl)
guard Task.isCancelled == false else { return }
await MainActor.run {
#if canImport(UIKit)
self?.uiImage = image
#elseif canImport(AppKit)
self?.nsImage = image
#endif
}
#endif
}
}
@@ -187,7 +126,6 @@ final class CachedRemoteImageLoader: ObservableObject {
return "data-image:\(source.count):\(head):\(tail)"
}
#if canImport(UIKit) || canImport(AppKit)
private static func imageFromDataURL(_ source: String) -> PlatformImage? {
let lower = source.lowercased()
guard lower.hasPrefix("data:image"), let commaIndex = source.firstIndex(of: ",") else { return nil }
@@ -203,14 +141,6 @@ final class CachedRemoteImageLoader: ObservableObject {
.replacingOccurrences(of: " ", with: "")
guard let data = Data(base64Encoded: payload, options: [.ignoreUnknownCharacters]) else { return nil }
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#else
return nil
#endif
}
#endif
}
#endif

View File

@@ -1,6 +1,11 @@
import Foundation
import SwiftUI
enum DeepLinkRouteEffect: Equatable {
case navigateToOrder(OrderRouteContext, tab: MainTab)
case none
}
struct ContentView: View {
@State var root: RootFlow = .main
@State var selectedTab: MainTab = .home
@@ -35,7 +40,7 @@ struct ContentView: View {
if isBootstrappingSession {
sessionBootstrapLoadingView
} else {
MainTabView(selectedTab: $selectedTab, root: $root, tokenStore: tokenStore, appState: $appState)
MainTabView(selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState, enterAuth: enterAuthFlow)
}
}
}
@@ -269,24 +274,41 @@ struct ContentView: View {
self.pushDeepLinkObserver = nil
}
@MainActor
private func route(to destination: DeepLinkDestination) {
guard root == .main else { return }
/// Pure mapping from a deep-link destination to what `route(to:)` should
/// do - split out so it's testable without a live `ContentView`
/// instance (this reads no `@State`, `route(to:)` is what applies the
/// result to `appState`/`selectedTab`).
func routeEffect(for destination: DeepLinkDestination) -> DeepLinkRouteEffect {
switch destination {
case .orderTracking(let orderId, let shortId):
appState.pendingOrderDeepLink = OrderRouteContext(
orderId: orderId,
shortId: shortId,
paymentMethod: nil,
total: nil,
intent: .auto
return .navigateToOrder(
OrderRouteContext(
orderId: orderId,
shortId: shortId,
paymentMethod: nil,
total: nil,
intent: .auto
),
tab: .profile
)
selectedTab = .profile
case .screen(let name, let params):
// Promo/coupon screens (participating stores/products, §6's
// `targetScreen` convention) land here once they exist see
// decisions/2026-08-04-push-deeplink-routing-contract.md.
logger.debug("Unhandled deep-link screen: \(name, privacy: .public) params=\(params, privacy: .public)")
return .none
}
}
@MainActor
private func route(to destination: DeepLinkDestination) {
guard root == .main else { return }
switch routeEffect(for: destination) {
case .navigateToOrder(let context, let tab):
appState.pendingOrderDeepLink = context
selectedTab = tab
case .none:
break
}
}
@@ -385,6 +407,16 @@ struct ContentView: View {
isBootstrappingSession = false
appState = AppState()
selectedTab = .home
enterAuthFlow()
}
/// The only correct way to transition `root` to `.auth`: pairs the
/// switch with the token bump `LoginView` needs to run its entry
/// reveal animation. A bare `root = .auth` leaves the login screen's
/// hero/text/button stuck hidden - see
/// decisions/2026-08-06-login-entry-animation-bug.md.
@MainActor
private func enterAuthFlow() {
shouldPrepareAuthEntryAnimation = true
root = .auth
authEntryAnimationToken += 1

View File

@@ -1,9 +1,5 @@
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
// MARK: - Colors
@@ -66,13 +62,7 @@ struct AppTypography {
}
private static var isBrandFontAvailable: Bool {
#if canImport(UIKit)
return UIFont(name: fontFamily, size: 16) != nil
#elseif canImport(AppKit)
return NSFont(name: fontFamily, size: 16) != nil
#else
return false
#endif
UIFont(name: fontFamily, size: 16) != nil
}
}

View File

@@ -0,0 +1,63 @@
import OSLog
import SwiftUI
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
@main
struct PediFoodsApp: App {
@UIApplicationDelegateAdaptor(PediFoodsAppDelegate.self) private var appDelegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
logger.debug("onResume")
NotificationCenter.default.post(name: .appDidResume, object: nil)
case .inactive:
logger.debug("onPause")
case .background:
logger.debug("onStop")
@unknown default:
logger.debug("unknown scene phase: \(String(describing: newPhase))")
}
}
}
}
final class PediFoodsAppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
logger.debug("onInit")
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
logger.debug("onLaunch")
Task { @MainActor in
PushNotificationCoordinator.shared.startObservingDeviceToken()
PushNotificationCoordinator.shared.becomeNotificationCenterDelegate()
await PushNotificationCoordinator.shared.refreshRegistrationIfAuthorized()
await PushNotificationCoordinator.shared.syncCustomerAttributes()
}
return true
}
func applicationWillTerminate(_ application: UIApplication) {
logger.debug("onDestroy")
}
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
logger.debug("onLowMemory")
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: NSNotification.Name("didRegisterForRemoteNotificationsWithDeviceToken"), object: application, userInfo: ["deviceToken": deviceToken])
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
NotificationCenter.default.post(name: NSNotification.Name("didFailToRegisterForRemoteNotificationsWithError"), object: application, userInfo: ["error": error])
}
}

View File

@@ -3,10 +3,6 @@
"strings" : {
"" : {
},
" " : {
"comment" : "A placeholder text used to maintain vertical spacing in the UI.",
"isCommentAutoGenerated" : true
},
"- %@" : {
"comment" : "A text element displaying a negative monetary value, indicating a discount.",
@@ -311,10 +307,6 @@
"comment" : "A loading message displayed while bootstrapping the user's session.",
"isCommentAutoGenerated" : true
},
"Carrinho" : {
"comment" : "A label for the cart section of the app.",
"isCommentAutoGenerated" : true
},
"Cartão de Crédito" : {
},
@@ -453,9 +445,6 @@
"Do que você gostou?" : {
"comment" : "A prompt asking users to mention what they liked about a review.",
"isCommentAutoGenerated" : true
},
"Editar endereço" : {
},
"Em uma escala de 0 a 10, qual é a chance de você indicar o Pedi Foods para um amigo?" : {
"comment" : "A description under the NPS scale question in the order review view.",
@@ -575,38 +564,9 @@
"comment" : "The title of the checkout screen.",
"isCommentAutoGenerated" : true
},
"Hello [%@](https://skip.tools)!" : {
"comment" : "Welcome tab contents",
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Hola [%@](https://skip.tools)!"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bonjour [%@](https://skip.tools)!"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "こんにちは [%@](https://skip.tools)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "你好 [%@](https://skip.tools)"
}
}
}
},
"Home" : {
"comment" : "Tab bar item title for the Home tab",
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {
@@ -724,13 +684,6 @@
},
"Meus Cartões" : {
},
"Meus Endereços" : {
"comment" : "A label displayed at the top of the view.",
"isCommentAutoGenerated" : true
},
"Meus Pedidos" : {
},
"Minhas Avaliações" : {
"comment" : "A button that links to their reviews.",
@@ -833,9 +786,6 @@
}
}
}
},
"Notificações" : {
},
"Notificações desativadas" : {
@@ -845,10 +795,6 @@
},
"Novo Cartão" : {
},
"Novo endereço" : {
"comment" : "A label for a form to add a new address.",
"isCommentAutoGenerated" : true
},
"Novo por aqui?" : {
"comment" : "A text that appears at the bottom of the screen, inviting users to create an account.",
@@ -919,19 +865,11 @@
},
"Pedido %@" : {
},
"PediFoods" : {
"comment" : "The name of the app.",
"isCommentAutoGenerated" : true
},
"Pedir Novamente" : {
"comment" : "A button that allows a user to order the same items again.",
"isCommentAutoGenerated" : true
},
"Perfil" : {
"comment" : "A label for the \"Profile\" tab in the main tab view.",
"isCommentAutoGenerated" : true
},
"Pizza de varios sabores" : {
"comment" : "A title displayed above the main content of the view.",
"extractionState" : "stale",
@@ -949,36 +887,6 @@
"comment" : "The title of the privacy policy section.",
"isCommentAutoGenerated" : true
},
"Powered by [Skip](https://skip.tools)" : {
"comment" : "Link markdown text for the Powered by… label",
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Impulsado por [Skip](https://skip.tools)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fonctionnalités offertes par [Skip](https://skip.tools)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "[Skip](https://skip.tools) を使って動かしています"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "由[Skip](https://skip.tools)提供支持"
}
}
}
},
"Precisa de ajuda com esse pedido?" : {
},
@@ -997,10 +905,6 @@
"comment" : "A label indicating that a particular address is the user's primary address.",
"isCommentAutoGenerated" : true
},
"Privacidade" : {
"comment" : "The title of the privacy policy screen.",
"isCommentAutoGenerated" : true
},
"Progresso do Pedido" : {
},
@@ -1008,24 +912,12 @@
"comment" : "The text for a button that allows a user to re-evaluate an order.",
"isCommentAutoGenerated" : true
},
"Receber Código" : {
"comment" : "A button label that says \"Receive Code\".",
"isCommentAutoGenerated" : true
},
"Reenviar código" : {
"comment" : "A button that allows a user to request a new OTP code.",
"isCommentAutoGenerated" : true
},
"Remover" : {
"comment" : "A button that deletes the user's profile picture.",
"isCommentAutoGenerated" : true
},
"Remover loja dos favoritos" : {
},
"Resposta da loja" : {
"comment" : "A heading for the reply from the store in an order review.",
"isCommentAutoGenerated" : true
},
"RESTAURANT" : {
@@ -1156,9 +1048,6 @@
"Seu endereço não é atendido por esta loja.\nDeseja trocar mesmo assim?" : {
"comment" : "An alert that appears when a user tries to place an order from an address that is not served by the store. The alert offers the user the option to either keep the current address or to switch to a different one",
"isCommentAutoGenerated" : true
},
"Seu pedido está em andamento" : {
},
"Sim" : {
"comment" : "The text for a button that confirms an action. In this case, it confirms the user's choice to continue with the order despite the address not being served by the store.",

View File

@@ -1,8 +1,5 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
#if canImport(LCEssentials) && os(iOS)
#if canImport(LCEssentials)
import LCEssentials
#endif
@@ -45,7 +42,7 @@ enum NetworkError: Error, LocalizedError {
}
}
private struct ApiErrorDescriptor {
struct ApiErrorDescriptor {
let code: String?
let message: String?
}
@@ -102,7 +99,7 @@ final class ApiClient: @unchecked Sendable {
// resolving no response, no error, nothing the UI must still
// get an answer so it can stop showing "nothing happened."
try await withTimeout(seconds: 20) { [self] in
#if canImport(LCEssentials) && os(iOS)
#if canImport(LCEssentials)
try await sendWithLCEssentials(request)
#else
try await sendWithURLSession(request)
@@ -128,8 +125,8 @@ final class ApiClient: @unchecked Sendable {
}
}
private extension ApiClient {
#if canImport(LCEssentials) && os(iOS)
extension ApiClient {
#if canImport(LCEssentials)
func sendWithLCEssentials<T: Decodable>(_ request: ApiRequest) async throws -> T {
let urlString = try buildURL(path: request.path, query: request.queryItems, baseURL: request.baseURLOverride ?? ApiConfig.baseURL).absoluteString
let method = request.method
@@ -402,7 +399,7 @@ private extension ApiClient {
return false
}
#if canImport(LCEssentials) && os(iOS)
#if canImport(LCEssentials)
func serverMessage(from error: NSError) -> String? {
for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data", "message"] {
if let value = error.userInfo[key] as? String,

View File

@@ -1,11 +1,6 @@
import Foundation
#if canImport(UIKit)
import UIKit
typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias PlatformImage = NSImage
#endif
enum AppCacheTTL {
static let twoHours: TimeInterval = 2 * 60 * 60
@@ -61,7 +56,6 @@ final class AppContentCache: @unchecked Sendable {
}
}
#if canImport(UIKit) || canImport(AppKit)
final class AppImageCache: @unchecked Sendable {
static let shared = AppImageCache()
@@ -132,23 +126,6 @@ final class AppImageCache: @unchecked Sendable {
}
private func platformImage(from data: Data) -> PlatformImage? {
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#else
return nil
#endif
UIImage(data: data)
}
}
#endif
#if !(canImport(UIKit) || canImport(AppKit))
final class AppImageCache: @unchecked Sendable {
static let shared = AppImageCache()
private init() {}
func invalidateAll() {}
}
#endif

View File

@@ -1,7 +1,4 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct FeatureControlRawFlag: Codable, Equatable {
let enabled: Bool
@@ -271,13 +268,11 @@ final class FeatureControlService {
}
private func appVersion() -> String {
#if canImport(UIKit) || canImport(AppKit)
let version = (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let version, version.isEmpty == false {
return version
}
#endif
return "0.0.0"
}
}

View File

@@ -2,6 +2,25 @@ import Foundation
#if os(iOS)
import CoreLocation
/// The subset of `CLLocationManager` `LocationService` needs lets tests
/// substitute a fake instead of touching real hardware/OS permission state.
@MainActor
protocol LocationManaging: AnyObject {
var locationManagingDelegate: CLLocationManagerDelegate? { get set }
var desiredAccuracy: CLLocationAccuracy { get set }
var authorizationStatus: CLAuthorizationStatus { get }
var location: CLLocation? { get }
func requestWhenInUseAuthorization()
func requestLocation()
}
extension CLLocationManager: LocationManaging {
var locationManagingDelegate: CLLocationManagerDelegate? {
get { delegate }
set { delegate = newValue }
}
}
#endif
@MainActor
@@ -23,17 +42,22 @@ final class LocationService: NSObject {
#endif
#if os(iOS)
private let manager = CLLocationManager()
private let manager: LocationManaging
private var completion: ((LocationResult) -> Void)?
#endif
#if os(iOS)
init(manager: LocationManaging = CLLocationManager()) {
self.manager = manager
super.init()
manager.locationManagingDelegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
#else
override init() {
super.init()
#if os(iOS)
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
#endif
}
#endif
func requestLocation(_ completion: @escaping (LocationResult) -> Void) {
#if os(iOS)

View File

@@ -1,7 +1,4 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@MainActor
final class OrderRealtimeTracker {

View File

@@ -173,6 +173,14 @@ final class PushNotificationCoordinator: NSObject {
}
}
/// `UNNotification.userInfo` is `[AnyHashable: Any]`, which the compiler
/// can't prove `Sendable` but it's an immutable payload handed to us
/// once by the OS, so crossing the actor boundary with it is safe in
/// practice.
private struct UncheckedSendableBox<Value>: @unchecked Sendable {
let value: Value
}
extension PushNotificationCoordinator: UNUserNotificationCenterDelegate {
/// Without a delegate, iOS silently drops push notifications while the
/// app is foregrounded this is what makes them display as a banner too.
@@ -189,9 +197,9 @@ extension PushNotificationCoordinator: UNUserNotificationCenterDelegate {
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
let userInfo = UncheckedSendableBox(value: response.notification.request.content.userInfo)
Task { @MainActor in
PushNotificationCoordinator.shared.handleTap(userInfo: userInfo)
PushNotificationCoordinator.shared.handleTap(userInfo: userInfo.value)
}
completionHandler()
}

View File

@@ -80,6 +80,12 @@ private struct PersistedOrderReviewsState: Codable {
}
enum SessionStateStore {
/// Swappable for tests (isolated `UserDefaults(suiteName:)`), defaults
/// to the real app defaults in production. `UserDefaults` is thread-safe
/// on its own; `nonisolated(unsafe)` only opts out of Swift 6's static-
/// mutable-state check for the var itself, tests set it once up front.
nonisolated(unsafe) static var defaults: UserDefaults = .standard
private static let legacyAddressKey = "session.address.state.v1"
private static let addressKeyPrefix = "session.address.state.v2."
private static let activeUserKey = "session.active.user.v1"
@@ -94,12 +100,12 @@ enum SessionStateStore {
/// See docs/api/push-notifications-integration-guide.md §2b avoid
/// re-prompting the order-tracking fallback alert on every screen visit.
static func shouldPromptPushOptIn() -> Bool {
guard let last = UserDefaults.standard.object(forKey: pushOptInPromptKey) as? Date else { return true }
guard let last = defaults.object(forKey: pushOptInPromptKey) as? Date else { return true }
return Date().timeIntervalSince(last) > pushOptInCooldown
}
static func recordPushOptInPrompted() {
UserDefaults.standard.set(Date(), forKey: pushOptInPromptKey)
defaults.set(Date(), forKey: pushOptInPromptKey)
}
static func makeUserKey(profileId: String?, email: String?) -> String? {
@@ -123,14 +129,14 @@ enum SessionStateStore {
static func setActiveUserKey(_ userKey: String?) {
let trimmed = (userKey ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
UserDefaults.standard.removeObject(forKey: activeUserKey)
defaults.removeObject(forKey: activeUserKey)
} else {
UserDefaults.standard.set(trimmed, forKey: activeUserKey)
defaults.set(trimmed, forKey: activeUserKey)
}
}
static func loadActiveUserKey() -> String? {
let value = UserDefaults.standard.string(forKey: activeUserKey)?
let value = defaults.string(forKey: activeUserKey)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let value, value.isEmpty == false {
return value
@@ -146,7 +152,6 @@ enum SessionStateStore {
}
static func loadAddress() -> AddressState? {
let defaults = UserDefaults.standard
let activeKey = loadActiveUserKey()
let scopedKey = addressStorageKey(for: activeKey)
@@ -214,17 +219,16 @@ enum SessionStateStore {
longitude: state.longitude
)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: addressStorageKey(for: nil))
defaults.set(data, forKey: addressStorageKey(for: nil))
}
static func clearAddress() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: addressStorageKey(for: nil))
defaults.removeObject(forKey: legacyAddressKey)
}
static func clearActiveUser() {
UserDefaults.standard.removeObject(forKey: activeUserKey)
defaults.removeObject(forKey: activeUserKey)
}
private static func cartStorageKey(for userKey: String?) -> String {
@@ -235,7 +239,6 @@ enum SessionStateStore {
}
static func loadCart() -> CartState? {
let defaults = UserDefaults.standard
let key = cartStorageKey(for: nil)
if let data = defaults.data(forKey: key),
@@ -297,11 +300,11 @@ enum SessionStateStore {
total: state.total
)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: cartStorageKey(for: nil))
defaults.set(data, forKey: cartStorageKey(for: nil))
}
static func clearCart() {
UserDefaults.standard.removeObject(forKey: cartStorageKey(for: nil))
defaults.removeObject(forKey: cartStorageKey(for: nil))
}
private static func trackedOrdersStorageKey(for userKey: String?) -> String {
@@ -336,7 +339,6 @@ enum SessionStateStore {
}
static func loadTrackedOrders() -> [PublicOrderResult] {
let defaults = UserDefaults.standard
let key = trackedOrdersStorageKey(for: nil)
guard let data = defaults.data(forKey: key),
let decoded = try? JSONDecoder().decode(PersistedTrackedOrdersState.self, from: data) else {
@@ -361,11 +363,11 @@ enum SessionStateStore {
}
let payload = PersistedTrackedOrdersState(orders: orders)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: trackedOrdersStorageKey(for: nil))
defaults.set(data, forKey: trackedOrdersStorageKey(for: nil))
}
static func clearTrackedOrders() {
UserDefaults.standard.removeObject(forKey: trackedOrdersStorageKey(for: nil))
defaults.removeObject(forKey: trackedOrdersStorageKey(for: nil))
}
static func savePendingCartOrderId(_ orderId: String) {
@@ -374,11 +376,11 @@ enum SessionStateStore {
clearPendingCartOrder()
return
}
UserDefaults.standard.set(clean, forKey: pendingCartOrderStorageKey(for: nil))
defaults.set(clean, forKey: pendingCartOrderStorageKey(for: nil))
}
static func loadPendingCartOrderId() -> String? {
let value = UserDefaults.standard.string(forKey: pendingCartOrderStorageKey(for: nil))?
let value = defaults.string(forKey: pendingCartOrderStorageKey(for: nil))?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let value, value.isEmpty == false {
return value
@@ -387,12 +389,12 @@ enum SessionStateStore {
}
static func clearPendingCartOrder() {
UserDefaults.standard.removeObject(forKey: pendingCartOrderStorageKey(for: nil))
defaults.removeObject(forKey: pendingCartOrderStorageKey(for: nil))
}
static func loadOrderReviews() -> [OrderReviewRecord] {
let key = orderReviewsStorageKey(for: nil)
guard let data = UserDefaults.standard.data(forKey: key),
guard let data = defaults.data(forKey: key),
let decoded = try? JSONDecoder().decode(PersistedOrderReviewsState.self, from: data) else {
return []
}
@@ -429,12 +431,12 @@ enum SessionStateStore {
let payload = PersistedOrderReviewsState(reviews: reviews)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: orderReviewsStorageKey(for: nil))
defaults.set(data, forKey: orderReviewsStorageKey(for: nil))
}
static func loadOrderReviewDraft(orderId: String) -> OrderReviewDraftState? {
let key = orderReviewDraftStorageKey(for: orderId, userKey: nil)
guard let data = UserDefaults.standard.data(forKey: key),
guard let data = defaults.data(forKey: key),
let draft = try? JSONDecoder().decode(OrderReviewDraftState.self, from: data) else {
return nil
}
@@ -444,11 +446,11 @@ enum SessionStateStore {
static func saveOrderReviewDraft(_ draft: OrderReviewDraftState) {
let key = orderReviewDraftStorageKey(for: draft.orderId, userKey: nil)
guard let data = try? JSONEncoder().encode(draft) else { return }
UserDefaults.standard.set(data, forKey: key)
defaults.set(data, forKey: key)
}
static func clearOrderReviewDraft(orderId: String) {
let key = orderReviewDraftStorageKey(for: orderId, userKey: nil)
UserDefaults.standard.removeObject(forKey: key)
defaults.removeObject(forKey: key)
}
}

Some files were not shown because too many files have changed in this diff Show More