1.0.4: Smarter auto-assign, quick dish entry, calendar fix, frictionless onboarding
- AutocompleteEngine: no-repeat rule now applies to all dishes (not just tagless ones); fallback still allows repeats when no alternative exists - CalendarService: update existing events instead of delete+create to prevent duplicates; fix weekStartDate using plan date instead of currentWeekStart - DishFormView: form stays open after saving in create mode for quick multi-dish entry; Done button to close; saved feedback banner - OnboardingViewModel/FirstDishesStepView: dishes are now optional during onboarding; Finish button always enabled - AnalyticsService: new events — onboarding_step_viewed(step) and auto_assign_used(filled, unfilled) - Localization: dish_done and first_dishes_optional_hint added in 6 languages; release notes updated in all 6 languages - Version bump: 1.0.3 → 1.0.4 (build 19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# MealMood — Project Context for Claude Code
|
||||
|
||||
## App
|
||||
- **Name**: MealMood: Family Meal Planner
|
||||
- **Bundle ID**: com.alexandrevazquez.mealmood
|
||||
- **Platform**: iOS/iPadOS (also runs as iOS app on Mac)
|
||||
- **Current version**: 1.0.4
|
||||
- **Main branch**: `1.0.1` (production), feature branches named after versions
|
||||
|
||||
## Credentials — all managed via `pass` (GPG-encrypted, syncs to Gitea)
|
||||
|
||||
Never hardcode credentials. Always read from `pass`:
|
||||
|
||||
```bash
|
||||
# Apple
|
||||
pass show apple/mealmood/apple-id # alexandre.vazquez@gmail.com
|
||||
pass show apple/mealmood/developer-team-id # 2825Q76T7H
|
||||
pass show apple/mealmood/appstore-connect-team-id # 128443966
|
||||
pass show apple/mealmood/app-store-app-id # 6759255553
|
||||
pass show apple/mealmood/app-specific-password-fastlane # for fastlane uploads
|
||||
|
||||
# Firebase
|
||||
pass show firebase/mealmood/api-key
|
||||
pass show firebase/mealmood/project-id
|
||||
pass show firebase/mealmood/google-app-id
|
||||
|
||||
# AdMob
|
||||
pass show admob/mealmood/app-id
|
||||
pass show admob/mealmood/banner-home-unit-id
|
||||
```
|
||||
|
||||
### TestFlight upload
|
||||
```bash
|
||||
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=$(pass show apple/mealmood/app-specific-password-fastlane)
|
||||
fastlane beta
|
||||
```
|
||||
|
||||
## Stack
|
||||
- SwiftUI + SwiftData (iOS 17+)
|
||||
- Firebase Analytics (`AnalyticsService.swift`)
|
||||
- Google AdMob (banner, free tier only, hidden on Mac)
|
||||
- StoreKit 2 (subscription `com.mealmood.premium.monthly`)
|
||||
- iCloud KV sync (`ICloudSyncService.swift`) — does NOT sync `isPremium`
|
||||
- Widget extension (`MealMoodWidget/`) — requires App Group `group.com.alexandrevazquez.mealmood`
|
||||
- Fastlane for CI/CD
|
||||
|
||||
## Premium logic
|
||||
- `PremiumSyncService` is the source of truth for premium state transitions
|
||||
- `isPremium` is determined **only** by StoreKit — never by iCloud sync
|
||||
- `Transaction.updates` listener in `ContentView` catches purchases in real-time
|
||||
- Tests: `MealMoodTests/PremiumSyncServiceTests.swift`
|
||||
|
||||
## Key files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `MealMood/ContentView.swift` | App root, premium sync, Transaction.updates listener |
|
||||
| `MealMood/Services/PremiumSyncService.swift` | Premium state machine (testable) |
|
||||
| `MealMood/Services/StoreManager.swift` | StoreKit 2 purchase/restore |
|
||||
| `MealMood/Services/ICloudSyncService.swift` | Cross-device sync (dishes, tags, plans — NOT isPremium) |
|
||||
| `MealMood/Views/Home/WeekPlanShareView.swift` | Export styles (default, school, vertical A4) |
|
||||
| `MealMoodWidget/MealMoodWidget.swift` | Home screen widget |
|
||||
| `fastlane/Fastfile` | `beta` and `release` lanes |
|
||||
| `fastlane/metadata/` | App Store metadata in 6 languages |
|
||||
@@ -10,15 +10,21 @@
|
||||
016D78C0DC1B1F6E671F36F7 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */; };
|
||||
0EC555FE2F4498780005513D /* aaaa.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 0EC555FD2F4498780005513D /* aaaa.storekit */; };
|
||||
0F91C8611D3BBE1FBD51D1E6 /* MealSlot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797A41D292142240FBDF9F2A /* MealSlot.swift */; };
|
||||
17E1054EE183486E7BE38C19 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */; };
|
||||
19330C6ECE7A60110A3A679D /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB00DA865F6FE86952BFC3 /* TagListView.swift */; };
|
||||
203DDFF14C0688DEDB6EE83E /* Localization+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0164A33496A304E40685EA6D /* Localization+Helpers.swift */; };
|
||||
233B9C1A5CA7E938318491C0 /* SocialShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */; };
|
||||
2519A55C6BDA686B6F85E79D /* View+MealCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC01EE9D78D980727A644CDF /* View+MealCard.swift */; };
|
||||
25B3C328E088A403CABAF20A /* PremiumSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */; };
|
||||
27D256A032E69F397E9552F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2D1AAC0DB21EA5E2CCA38AD /* SwiftUI.framework */; };
|
||||
294D7F9BE5A2907F74B8614C /* Date+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DE70CD2A01FCE5370A46DD3 /* Date+Helpers.swift */; };
|
||||
2A58E7C96AF7C14EBFE1C65C /* PremiumSyncServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2F6B2694CEC97AE3C1B13 /* PremiumSyncServiceTests.swift */; };
|
||||
30540EF74F2C97F741BA6A1A /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AE68D5B577514ADD6BACB /* Tag.swift */; };
|
||||
31DFC035AE34F49FB35071A7 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89C47DD7B8F4B1441780E611 /* HomeView.swift */; };
|
||||
37F5CEC4862C0AE2F3385ABA /* DishListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BD0E078644749E93B0D2B /* DishListView.swift */; };
|
||||
3C5225918DA8DE77F5AC1AA0 /* MealMoodApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */; };
|
||||
4218F220B7412965A5001A88 /* CalendarStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76719058DD3A4177B2B4163E /* CalendarStepView.swift */; };
|
||||
478AE18DA0F887D3D5164635 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C77C350D4A839B76A3D2EFE7 /* Foundation.framework */; };
|
||||
48F9D3308375F820B2B88D1B /* PremiumUpsellBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */; };
|
||||
4D53B5101AE845D7C03A0EB4 /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = 192E7CF40FD34C3FF4DDFDA3 /* GoogleMobileAds */; };
|
||||
511DDB82FF4DD30018101418 /* DishDrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23957C735075B0DFB1DF51D7 /* DishDrawerView.swift */; };
|
||||
@@ -44,6 +50,8 @@
|
||||
9BC60C30918C117608E43699 /* DefaultDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E00AB23342224B3407838B /* DefaultDataService.swift */; };
|
||||
9BE86765B8F0368AD5C86FC4 /* TagViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9BFFE7A4BC6961815D2334 /* TagViewModel.swift */; };
|
||||
9D40600B9AC21F0FF64B77F9 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 103374A7D91B4F884278DA72 /* StoreManager.swift */; };
|
||||
9EE4C3257DCE32D2872CD435 /* MealMoodWidget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 574CB4BDE993DA0CEC251ABD /* MealMoodWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
A4D50C0B4E91B3542C63017A /* MealMoodWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B042BB2F81973D854E84BABD /* MealMoodWidget.swift */; };
|
||||
A6F130EC376B7BC270AD908D /* TagPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1C12C3A693D7C41E9F80B /* TagPill.swift */; };
|
||||
A7CAD588CF78F404C8065CF1 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDEE086FD20D16DE507B247 /* HomeViewModel.swift */; };
|
||||
A9278F6559777CB9FA7FB69E /* AdMobConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812303C94C8C1D2FADF3CB37 /* AdMobConfig.swift */; };
|
||||
@@ -56,8 +64,10 @@
|
||||
B8D7CA62789429C2F693E4F6 /* MealMoodLogoMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC64110469AA91D5F28C423 /* MealMoodLogoMark.swift */; };
|
||||
BE908B23F64F33D594AB8D82 /* DateHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */; };
|
||||
C14BDE4C3EF6AF75F378D892 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */; };
|
||||
C3CB4ACC213DAAD25E610C56 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB4FC89C90928DD833240244 /* WidgetKit.framework */; };
|
||||
C565A1975302748A2B2152BE /* DishViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8472961AE65E2A3460B67C3 /* DishViewModel.swift */; };
|
||||
C81C935386BE1E939DA55F8D /* MealWindowsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */; };
|
||||
CD174669421FE1FD4315F6E0 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */; };
|
||||
CD2ADCDADFEC2D562E23D8E6 /* AutocompleteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */; };
|
||||
D2840F9D92D16026338CE83C /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C00EED19123F00AE87FF24 /* AppSettings.swift */; };
|
||||
D8149D55E74F7BBFC0402F46 /* DishFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */; };
|
||||
@@ -70,6 +80,9 @@
|
||||
E6A57F29C02AE138D705BAF7 /* AppIconPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F2EE5A756C183B6D505403 /* AppIconPlaceholder.swift */; };
|
||||
EA4EC6244EEBE6D568BCA5E5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3944B4AD876455057EB2265 /* Assets.xcassets */; };
|
||||
F65AD80426AF8DA12FBD661C /* WeekendsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */; };
|
||||
FB0011111111111111111101 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0011111111111111111105 /* FirebaseAnalytics */; };
|
||||
FB0011111111111111111102 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111103 /* GoogleService-Info.plist */; };
|
||||
FB0011111111111111111106 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111107 /* AnalyticsService.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -82,6 +95,20 @@
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
DF9575A974376FF5C6420060 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
9EE4C3257DCE32D2872CD435 /* MealMoodWidget.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
00F2EE5A756C183B6D505403 /* AppIconPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPlaceholder.swift; sourceTree = "<group>"; };
|
||||
0164A33496A304E40685EA6D /* Localization+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Helpers.swift"; sourceTree = "<group>"; };
|
||||
@@ -97,6 +124,7 @@
|
||||
168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = "<group>"; };
|
||||
17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagLocalizationTests.swift; sourceTree = "<group>"; };
|
||||
17E00AB23342224B3407838B /* DefaultDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDataService.swift; sourceTree = "<group>"; };
|
||||
1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SocialShareSheet.swift; sourceTree = "<group>"; };
|
||||
20F23BF9788A3217E3D63721 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
||||
23957C735075B0DFB1DF51D7 /* DishDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishDrawerView.swift; sourceTree = "<group>"; };
|
||||
2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealMoodApp.swift; sourceTree = "<group>"; };
|
||||
@@ -106,9 +134,12 @@
|
||||
4EDEE086FD20D16DE507B247 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||
50E1C12C3A693D7C41E9F80B /* TagPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPill.swift; sourceTree = "<group>"; };
|
||||
5187023D83BBC5E8D304CE3A /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
574CB4BDE993DA0CEC251ABD /* MealMoodWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MealMoodWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5C9BFFE7A4BC6961815D2334 /* TagViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagViewModel.swift; sourceTree = "<group>"; };
|
||||
5D6A3FCADC4F4B7AAB73E5CA /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
60F0A5C2FF1B797A4BB69B39 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
623EDDC4CD134883FC9D242D /* Dish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dish.swift; sourceTree = "<group>"; };
|
||||
62E2F6B2694CEC97AE3C1B13 /* PremiumSyncServiceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PremiumSyncServiceTests.swift; sourceTree = "<group>"; };
|
||||
69B309A43E486B5A723324BA /* WeekCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekCalendarView.swift; sourceTree = "<group>"; };
|
||||
6D718B2E4F7E6D22E502F0A6 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
6EA1948B5DAEEEA09B78C8A9 /* SecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryButton.swift; sourceTree = "<group>"; };
|
||||
@@ -122,18 +153,25 @@
|
||||
89C47DD7B8F4B1441780E611 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumUpsellBanner.swift; sourceTree = "<group>"; };
|
||||
8EE10285FEE9BE0684CC66A3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PremiumSyncService.swift; sourceTree = "<group>"; };
|
||||
93B4B31A8F6E4D7AAB1B6A01 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
965555A7774FF7EB975F0A07 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
9F86EB5DDB126D73486FD956 /* MealMood.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MealMood.entitlements; sourceTree = "<group>"; };
|
||||
9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealWindowsStepView.swift; sourceTree = "<group>"; };
|
||||
A23267BAA116E1214870416C /* WeekPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekPlan.swift; sourceTree = "<group>"; };
|
||||
A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = "<group>"; };
|
||||
AB771E85924AE086647A5113 /* Color+MealMood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+MealMood.swift"; sourceTree = "<group>"; };
|
||||
B042BB2F81973D854E84BABD /* MealMoodWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MealMoodWidget.swift; sourceTree = "<group>"; };
|
||||
B0A631C6D0CF7A0724ACF6A5 /* CalendarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarService.swift; sourceTree = "<group>"; };
|
||||
B3369706BF941E09F88243F1 /* MealMoodWidget.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = MealMoodWidget.entitlements; sourceTree = "<group>"; };
|
||||
B3944B4AD876455057EB2265 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
B68E5C82608F60FF8798BB81 /* EmptySlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySlotView.swift; sourceTree = "<group>"; };
|
||||
B6CF1C9CC9BDB3B20B1834EB /* TagSelectorSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectorSheet.swift; sourceTree = "<group>"; };
|
||||
BAC64110469AA91D5F28C423 /* MealMoodLogoMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealMoodLogoMark.swift; sourceTree = "<group>"; };
|
||||
BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
BED090ADA1FB259AD2AFEDE2 /* ICloudSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudSyncService.swift; sourceTree = "<group>"; };
|
||||
C2D1AAC0DB21EA5E2CCA38AD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
C77C350D4A839B76A3D2EFE7 /* 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; };
|
||||
C7B13F4D4D362E50CA65A0C1 /* MealMood.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MealMood.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C7C00EED19123F00AE87FF24 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
CB3BD0E078644749E93B0D2B /* DishListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishListView.swift; sourceTree = "<group>"; };
|
||||
@@ -147,8 +185,11 @@
|
||||
E18A2F6B56D64C1EBC91E21B /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
E8472961AE65E2A3460B67C3 /* DishViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishViewModel.swift; sourceTree = "<group>"; };
|
||||
E8825212ACFA0D3F6C716E94 /* ReviewPromptService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptService.swift; sourceTree = "<group>"; };
|
||||
EB4FC89C90928DD833240244 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
F0200C0C11C912711C724558 /* PremiumAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumAccess.swift; sourceTree = "<group>"; };
|
||||
F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = MealMoodTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MealMoodTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FB0011111111111111111103 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
FB0011111111111111111107 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||
FC01EE9D78D980727A644CDF /* View+MealCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+MealCard.swift"; sourceTree = "<group>"; };
|
||||
FD4FD72583C004A5D5C939A3 /* FilledSlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledSlotView.swift; sourceTree = "<group>"; };
|
||||
FD621CE9B44512AE8894D92E /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
||||
@@ -157,11 +198,23 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
81780419712273974230423F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
478AE18DA0F887D3D5164635 /* Foundation.framework in Frameworks */,
|
||||
CD174669421FE1FD4315F6E0 /* WidgetKit.framework in Frameworks */,
|
||||
27D256A032E69F397E9552F3 /* SwiftUI.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9240A703A486116A9871C534 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4D53B5101AE845D7C03A0EB4 /* GoogleMobileAds in Frameworks */,
|
||||
FB0011111111111111111101 /* FirebaseAnalytics in Frameworks */,
|
||||
C3CB4ACC213DAAD25E610C56 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -174,6 +227,8 @@
|
||||
8C162D5DCA596B64BEE7AD7C /* MealMood */,
|
||||
7975F99C73940CF3F40F658E /* MealMoodTests */,
|
||||
5BEE00A9E25BDAA086585241 /* Products */,
|
||||
F4FE25386665429412E51DB4 /* Frameworks */,
|
||||
2BC91A087BC03FF7810C529F /* MealMoodWidget */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -184,10 +239,21 @@
|
||||
89C47DD7B8F4B1441780E611 /* HomeView.swift */,
|
||||
69B309A43E486B5A723324BA /* WeekCalendarView.swift */,
|
||||
2E113A29DEC0551C41AB7EBB /* WeekPlanShareView.swift */,
|
||||
1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2BC91A087BC03FF7810C529F /* MealMoodWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B042BB2F81973D854E84BABD /* MealMoodWidget.swift */,
|
||||
5D6A3FCADC4F4B7AAB73E5CA /* Info.plist */,
|
||||
B3369706BF941E09F88243F1 /* MealMoodWidget.entitlements */,
|
||||
);
|
||||
path = MealMoodWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
35F15C6817E504B1114420CD /* Premium */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -261,6 +327,7 @@
|
||||
children = (
|
||||
C7B13F4D4D362E50CA65A0C1 /* MealMood.app */,
|
||||
F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */,
|
||||
574CB4BDE993DA0CEC251ABD /* MealMoodWidget.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -272,6 +339,7 @@
|
||||
DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */,
|
||||
168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */,
|
||||
17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */,
|
||||
62E2F6B2694CEC97AE3C1B13 /* PremiumSyncServiceTests.swift */,
|
||||
);
|
||||
path = MealMoodTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -324,6 +392,7 @@
|
||||
8E15828872FAEA71A7384099 /* Localizable.strings */,
|
||||
9F86EB5DDB126D73486FD956 /* MealMood.entitlements */,
|
||||
4ED1758EE5CD0A3EB9D9BCDF /* Brand */,
|
||||
FB0011111111111111111103 /* GoogleService-Info.plist */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -367,10 +436,30 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECCF14900EFD2F0E1432DB05 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C77C350D4A839B76A3D2EFE7 /* Foundation.framework */,
|
||||
);
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F4FE25386665429412E51DB4 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECCF14900EFD2F0E1432DB05 /* iOS */,
|
||||
BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */,
|
||||
EB4FC89C90928DD833240244 /* WidgetKit.framework */,
|
||||
C2D1AAC0DB21EA5E2CCA38AD /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F9F4F35A66E1D459F23E1679 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
812303C94C8C1D2FADF3CB37 /* AdMobConfig.swift */,
|
||||
FB0011111111111111111107 /* AnalyticsService.swift */,
|
||||
CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */,
|
||||
B0A631C6D0CF7A0724ACF6A5 /* CalendarService.swift */,
|
||||
17E00AB23342224B3407838B /* DefaultDataService.swift */,
|
||||
@@ -380,6 +469,8 @@
|
||||
F0200C0C11C912711C724558 /* PremiumAccess.swift */,
|
||||
E8825212ACFA0D3F6C716E94 /* ReviewPromptService.swift */,
|
||||
103374A7D91B4F884278DA72 /* StoreManager.swift */,
|
||||
A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */,
|
||||
928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
@@ -399,12 +490,27 @@
|
||||
C4E8F43173BDFCAE4D6DFF49 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MealMoodTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = MealMoodTests;
|
||||
productReference = F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
C82BD86FE72A5E319983AEE3 /* MealMoodWidget */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 14CECD46CDA9DFDC2D928807 /* Build configuration list for PBXNativeTarget "MealMoodWidget" */;
|
||||
buildPhases = (
|
||||
A6B405631E28429E34B68811 /* Sources */,
|
||||
81780419712273974230423F /* Frameworks */,
|
||||
89994B0BC8A1CC21C26EC6F6 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = MealMoodWidget;
|
||||
productName = MealMoodWidget;
|
||||
productReference = 574CB4BDE993DA0CEC251ABD /* MealMoodWidget.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
DE69ED74719D07B4A5770741 /* MealMood */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BE85686E0B47779700A19EA0 /* Build configuration list for PBXNativeTarget "MealMood" */;
|
||||
@@ -412,6 +518,7 @@
|
||||
B2A7B3CE9D5681C52D906D1A /* Sources */,
|
||||
60AC9519630B6008787A8859 /* Resources */,
|
||||
9240A703A486116A9871C534 /* Frameworks */,
|
||||
DF9575A974376FF5C6420060 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -420,6 +527,7 @@
|
||||
name = MealMood;
|
||||
packageProductDependencies = (
|
||||
192E7CF40FD34C3FF4DDFDA3 /* GoogleMobileAds */,
|
||||
FB0011111111111111111105 /* FirebaseAnalytics */,
|
||||
);
|
||||
productName = MealMood;
|
||||
productReference = C7B13F4D4D362E50CA65A0C1 /* MealMood.app */;
|
||||
@@ -451,12 +559,15 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
BE33D996A0D3FB7D5E000A7F /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */,
|
||||
FB0011111111111111111104 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
|
||||
);
|
||||
productRefGroup = 5BEE00A9E25BDAA086585241 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
DE69ED74719D07B4A5770741 /* MealMood */,
|
||||
71CBF84DA3DD6F325A40C1A1 /* MealMoodTests */,
|
||||
C82BD86FE72A5E319983AEE3 /* MealMoodWidget */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -470,12 +581,28 @@
|
||||
EA4EC6244EEBE6D568BCA5E5 /* Assets.xcassets in Resources */,
|
||||
87EB371FD3B6C241AC721C55 /* Localizable.strings in Resources */,
|
||||
7CBE113B28462C6C41C35533 /* MealMoodLogo.svg in Resources */,
|
||||
FB0011111111111111111102 /* GoogleService-Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
89994B0BC8A1CC21C26EC6F6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
A6B405631E28429E34B68811 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A4D50C0B4E91B3542C63017A /* MealMoodWidget.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B1ACBA2CBB77DB5CAD57EB57 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -484,6 +611,7 @@
|
||||
BE908B23F64F33D594AB8D82 /* DateHelpersTests.swift in Sources */,
|
||||
016D78C0DC1B1F6E671F36F7 /* OnboardingViewModelTests.swift in Sources */,
|
||||
ACAEFA5F4C170C6438CD2652 /* TagLocalizationTests.swift in Sources */,
|
||||
2A58E7C96AF7C14EBFE1C65C /* PremiumSyncServiceTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -493,6 +621,7 @@
|
||||
files = (
|
||||
B3C9E5149B70C477EACBDFFB /* AdBannerView.swift in Sources */,
|
||||
A9278F6559777CB9FA7FB69E /* AdMobConfig.swift in Sources */,
|
||||
FB0011111111111111111106 /* AnalyticsService.swift in Sources */,
|
||||
E6A57F29C02AE138D705BAF7 /* AppIconPlaceholder.swift in Sources */,
|
||||
D2840F9D92D16026338CE83C /* AppSettings.swift in Sources */,
|
||||
CD2ADCDADFEC2D562E23D8E6 /* AutocompleteEngine.swift in Sources */,
|
||||
@@ -545,6 +674,9 @@
|
||||
6C808D1B6E8534FABC4A4053 /* WeekPlanShareView.swift in Sources */,
|
||||
F65AD80426AF8DA12FBD661C /* WeekendsStepView.swift in Sources */,
|
||||
E2524335ED3A5DB7813BEA99 /* WelcomeStepView.swift in Sources */,
|
||||
17E1054EE183486E7BE38C19 /* WidgetDataStore.swift in Sources */,
|
||||
233B9C1A5CA7E938318491C0 /* SocialShareSheet.swift in Sources */,
|
||||
25B3C328E088A403CABAF20A /* PremiumSyncService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -631,6 +763,47 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
70E1B27C392FF6A967A2CEF0 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMoodWidget/MealMoodWidget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
INFOPLIST_FILE = MealMoodWidget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood.widget;
|
||||
PRODUCT_NAME = MealMoodWidget;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
78DB4A7025FD82DEF656DD24 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMoodWidget/MealMoodWidget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
INFOPLIST_FILE = MealMoodWidget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood.widget;
|
||||
PRODUCT_NAME = MealMoodWidget;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A29AC9249B9DE06546506655 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -655,7 +828,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MealMood/Resources/Info.plist;
|
||||
@@ -739,6 +912,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -759,7 +933,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MealMood/Resources/Info.plist;
|
||||
@@ -779,6 +953,15 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
14CECD46CDA9DFDC2D928807 /* Build configuration list for PBXNativeTarget "MealMoodWidget" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
70E1B27C392FF6A967A2CEF0 /* Release */,
|
||||
78DB4A7025FD82DEF656DD24 /* Debug */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
82E16632C9B76AC33DE64CBB /* Build configuration list for PBXProject "MealMood" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -817,6 +1000,14 @@
|
||||
minimumVersion = 11.13.0;
|
||||
};
|
||||
};
|
||||
FB0011111111111111111104 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 11.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -825,6 +1016,11 @@
|
||||
package = BE33D996A0D3FB7D5E000A7F /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */;
|
||||
productName = GoogleMobileAds;
|
||||
};
|
||||
FB0011111111111111111105 /* FirebaseAnalytics */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FB0011111111111111111104 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
|
||||
productName = FirebaseAnalytics;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 7C11D794B7E45A145D5ADBFD /* Project object */;
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>1.0.4</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>21</string>
|
||||
<key>GADApplicationIdentifier</key>
|
||||
<string>ca-app-pub-1549720748100858~9985112590</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Schritt 4: Füge deine Lieblingsgerichte hinzu";
|
||||
"first_dishes_subtitle" = "Füge mindestens 2 Gerichte hinzu, um zu starten";
|
||||
"first_dishes_subtitle" = "Füge einige Gerichte hinzu, um schneller zu starten.";
|
||||
"first_dishes_optional_hint" = "Optional — du kannst jederzeit weitere Gerichte in der App hinzufügen.";
|
||||
"first_dishes_name_placeholder" = "Gerichtsname";
|
||||
"first_dishes_add_tag" = "Tag hinzufügen";
|
||||
"first_dishes_add_dish" = "Gericht hinzufügen";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "Dieses Gericht löschen?";
|
||||
"dish_delete_message" = "Diese Aktion kann nicht rückgängig gemacht werden";
|
||||
"dish_cancel" = "Abbrechen";
|
||||
"dish_done" = "Fertig";
|
||||
"dish_delete_blocked_title" = "Löschen nicht möglich";
|
||||
"dish_delete_blocked_message" = "Du kannst kein Gericht löschen, das der aktuellen Woche zugewiesen ist.";
|
||||
"dish_delete_blocked_ok" = "OK";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 Stunden vorher";
|
||||
|
||||
"first_dishes_limit_reached" = "Du hast 10 Gerichte erreicht. Tippe auf Fertig, wenn du bereit bist.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Als Story-Hintergrund teilen";
|
||||
"share_copy_image" = "Bild kopieren";
|
||||
"share_copy_subtitle" = "In WhatsApp, Telegram einfügen…";
|
||||
"share_more_options" = "Mehr Optionen";
|
||||
"share_more_subtitle" = "In jeder App teilen";
|
||||
"share_export_sheet_title" = "Teilen";
|
||||
"share_copied" = "Bild kopiert!";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "So könnte deine Woche aussehen ✨";
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Step 4: Add your go-to dishes";
|
||||
"first_dishes_subtitle" = "Add at least 2 dishes to get started";
|
||||
"first_dishes_subtitle" = "Add a few dishes to get started faster.";
|
||||
"first_dishes_optional_hint" = "Optional — you can add more dishes anytime from the app.";
|
||||
"first_dishes_name_placeholder" = "Dish name";
|
||||
"first_dishes_add_tag" = "Add tag";
|
||||
"first_dishes_add_dish" = "Add dish";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "Delete this dish?";
|
||||
"dish_delete_message" = "This action cannot be undone";
|
||||
"dish_cancel" = "Cancel";
|
||||
"dish_done" = "Done";
|
||||
"dish_delete_blocked_title" = "Cannot delete";
|
||||
"dish_delete_blocked_message" = "You cannot delete a dish assigned in the current week.";
|
||||
"dish_delete_blocked_ok" = "OK";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 hours before";
|
||||
|
||||
"first_dishes_limit_reached" = "You reached 10 dishes. Tap Finish when you are ready.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Share as a Story background";
|
||||
"share_copy_image" = "Copy Image";
|
||||
"share_copy_subtitle" = "Paste in WhatsApp, Telegram…";
|
||||
"share_more_options" = "More options";
|
||||
"share_more_subtitle" = "Share to any app";
|
||||
"share_export_sheet_title" = "Share";
|
||||
"share_copied" = "Image copied!";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "Your week could look like this ✨";
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Paso 4: Añade tus platos habituales";
|
||||
"first_dishes_subtitle" = "Añade al menos 2 platos para empezar";
|
||||
"first_dishes_subtitle" = "Añade algunos platos para empezar más rápido.";
|
||||
"first_dishes_optional_hint" = "Opcional — puedes añadir más platos desde la app en cualquier momento.";
|
||||
"first_dishes_name_placeholder" = "Nombre del plato";
|
||||
"first_dishes_add_tag" = "Añadir etiqueta";
|
||||
"first_dishes_add_dish" = "Añadir plato";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "¿Eliminar este plato?";
|
||||
"dish_delete_message" = "Esta acción no se puede deshacer";
|
||||
"dish_cancel" = "Cancelar";
|
||||
"dish_done" = "Hecho";
|
||||
"dish_delete_blocked_title" = "No se puede eliminar";
|
||||
"dish_delete_blocked_message" = "No puedes eliminar un plato asignado en la semana actual.";
|
||||
"dish_delete_blocked_ok" = "Entendido";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 horas antes";
|
||||
|
||||
"first_dishes_limit_reached" = "Has llegado a 10 platos. Pulsa Finalizar cuando quieras.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Compartir como fondo de historia";
|
||||
"share_copy_image" = "Copiar imagen";
|
||||
"share_copy_subtitle" = "Pega en WhatsApp, Telegram…";
|
||||
"share_more_options" = "Más opciones";
|
||||
"share_more_subtitle" = "Compartir con cualquier app";
|
||||
"share_export_sheet_title" = "Compartir";
|
||||
"share_copied" = "¡Imagen copiada!";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "Tu semana puede quedar así ✨";
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Étape 4 : Ajoutez vos plats favoris";
|
||||
"first_dishes_subtitle" = "Ajoutez au moins 2 plats pour commencer";
|
||||
"first_dishes_subtitle" = "Ajoutez quelques plats pour démarrer plus vite.";
|
||||
"first_dishes_optional_hint" = "Optionnel — vous pouvez en ajouter d'autres depuis l'app.";
|
||||
"first_dishes_name_placeholder" = "Nom du plat";
|
||||
"first_dishes_add_tag" = "Ajouter un tag";
|
||||
"first_dishes_add_dish" = "Ajouter un plat";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "Supprimer ce plat ?";
|
||||
"dish_delete_message" = "Cette action est irréversible";
|
||||
"dish_cancel" = "Annuler";
|
||||
"dish_done" = "Terminé";
|
||||
"dish_delete_blocked_title" = "Suppression impossible";
|
||||
"dish_delete_blocked_message" = "Vous ne pouvez pas supprimer un plat assigné à la semaine en cours.";
|
||||
"dish_delete_blocked_ok" = "OK";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 heures avant";
|
||||
|
||||
"first_dishes_limit_reached" = "Vous avez atteint 10 plats. Touchez Terminer quand vous êtes prêt.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Partager comme fond de Story";
|
||||
"share_copy_image" = "Copier l'image";
|
||||
"share_copy_subtitle" = "Coller dans WhatsApp, Telegram…";
|
||||
"share_more_options" = "Plus d'options";
|
||||
"share_more_subtitle" = "Partager dans n'importe quelle app";
|
||||
"share_export_sheet_title" = "Partager";
|
||||
"share_copied" = "Image copiée !";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "Votre semaine pourrait ressembler à ça ✨";
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Passo 4: Aggiungi i tuoi piatti preferiti";
|
||||
"first_dishes_subtitle" = "Aggiungi almeno 2 piatti per iniziare";
|
||||
"first_dishes_subtitle" = "Aggiungi alcuni piatti per iniziare più velocemente.";
|
||||
"first_dishes_optional_hint" = "Opzionale — puoi aggiungerne altri dall'app in qualsiasi momento.";
|
||||
"first_dishes_name_placeholder" = "Nome del piatto";
|
||||
"first_dishes_add_tag" = "Aggiungi tag";
|
||||
"first_dishes_add_dish" = "Aggiungi piatto";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "Eliminare questo piatto?";
|
||||
"dish_delete_message" = "Questa azione non può essere annullata";
|
||||
"dish_cancel" = "Annulla";
|
||||
"dish_done" = "Fatto";
|
||||
"dish_delete_blocked_title" = "Impossibile eliminare";
|
||||
"dish_delete_blocked_message" = "Non puoi eliminare un piatto assegnato alla settimana corrente.";
|
||||
"dish_delete_blocked_ok" = "OK";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 ore prima";
|
||||
|
||||
"first_dishes_limit_reached" = "Hai raggiunto 10 piatti. Tocca Fine quando sei pronto.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Condividi come sfondo di una Story";
|
||||
"share_copy_image" = "Copia immagine";
|
||||
"share_copy_subtitle" = "Incolla in WhatsApp, Telegram…";
|
||||
"share_more_options" = "Altre opzioni";
|
||||
"share_more_subtitle" = "Condividi con qualsiasi app";
|
||||
"share_export_sheet_title" = "Condividi";
|
||||
"share_copied" = "Immagine copiata!";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "La tua settimana potrebbe essere così ✨";
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Etapa 4: Adicione seus pratos favoritos";
|
||||
"first_dishes_subtitle" = "Adicione pelo menos 2 pratos para começar";
|
||||
"first_dishes_subtitle" = "Adicione alguns pratos para começar mais rápido.";
|
||||
"first_dishes_optional_hint" = "Opcional — você pode adicionar mais pratos pelo app a qualquer momento.";
|
||||
"first_dishes_name_placeholder" = "Nome do prato";
|
||||
"first_dishes_add_tag" = "Adicionar tag";
|
||||
"first_dishes_add_dish" = "Adicionar prato";
|
||||
@@ -104,6 +105,7 @@
|
||||
"dish_delete_title" = "Excluir este prato?";
|
||||
"dish_delete_message" = "Essa ação não pode ser desfeita";
|
||||
"dish_cancel" = "Cancelar";
|
||||
"dish_done" = "Concluído";
|
||||
"dish_delete_blocked_title" = "Não é possível excluir";
|
||||
"dish_delete_blocked_message" = "Você não pode excluir um prato atribuído à semana atual.";
|
||||
"dish_delete_blocked_ok" = "OK";
|
||||
@@ -284,3 +286,15 @@
|
||||
"reminder_120" = "2 horas antes";
|
||||
|
||||
"first_dishes_limit_reached" = "Você chegou a 10 pratos. Toque em Concluir quando estiver pronto.";
|
||||
|
||||
// Social share
|
||||
"share_instagram_subtitle" = "Compartilhar como fundo de Story";
|
||||
"share_copy_image" = "Copiar imagem";
|
||||
"share_copy_subtitle" = "Cole no WhatsApp, Telegram…";
|
||||
"share_more_options" = "Mais opções";
|
||||
"share_more_subtitle" = "Compartilhar com qualquer app";
|
||||
"share_export_sheet_title" = "Compartilhar";
|
||||
"share_copied" = "Imagem copiada!";
|
||||
|
||||
// Onboarding preview
|
||||
"onboarding_welcome_preview_label" = "Sua semana pode ficar assim ✨";
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import FirebaseAnalytics
|
||||
|
||||
enum AnalyticsEvent {
|
||||
// Onboarding
|
||||
static let onboardingCompleted = "onboarding_completed"
|
||||
|
||||
// Dishes
|
||||
static let dishAdded = "dish_added"
|
||||
static let dishDeleted = "dish_deleted"
|
||||
|
||||
// Planner
|
||||
static let mealAssigned = "meal_assigned"
|
||||
static let mealCleared = "meal_cleared"
|
||||
static let weekReset = "week_reset"
|
||||
static let weekPlanShared = "week_plan_shared"
|
||||
|
||||
// Premium
|
||||
static let premiumUpgradeTapped = "premium_upgrade_tapped"
|
||||
static let premiumPurchased = "premium_purchased"
|
||||
static let premiumRestored = "premium_restored"
|
||||
|
||||
// Planner
|
||||
static let autoAssignUsed = "auto_assign_used"
|
||||
|
||||
// Onboarding
|
||||
static let onboardingStepViewed = "onboarding_step_viewed"
|
||||
|
||||
// Settings
|
||||
static let iCloudSyncToggled = "icloud_sync_toggled"
|
||||
static let notificationsToggled = "notifications_toggled"
|
||||
static let languageChanged = "language_changed"
|
||||
}
|
||||
|
||||
enum AnalyticsService {
|
||||
static func logEvent(_ name: String, parameters: [String: Any]? = nil) {
|
||||
Analytics.logEvent(name, parameters: parameters)
|
||||
}
|
||||
|
||||
static func logScreenView(_ screenName: String) {
|
||||
Analytics.logEvent(AnalyticsEventScreenView, parameters: [
|
||||
AnalyticsParameterScreenName: screenName,
|
||||
AnalyticsParameterScreenClass: screenName
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Convenience methods
|
||||
|
||||
static func logDishAdded(tagCount: Int) {
|
||||
logEvent(AnalyticsEvent.dishAdded, parameters: ["tag_count": tagCount])
|
||||
}
|
||||
|
||||
static func logDishDeleted() {
|
||||
logEvent(AnalyticsEvent.dishDeleted)
|
||||
}
|
||||
|
||||
static func logMealAssigned() {
|
||||
logEvent(AnalyticsEvent.mealAssigned)
|
||||
}
|
||||
|
||||
static func logMealCleared() {
|
||||
logEvent(AnalyticsEvent.mealCleared)
|
||||
}
|
||||
|
||||
static func logWeekReset() {
|
||||
logEvent(AnalyticsEvent.weekReset)
|
||||
}
|
||||
|
||||
static func logWeekPlanShared(format: String) {
|
||||
logEvent(AnalyticsEvent.weekPlanShared, parameters: ["format": format])
|
||||
}
|
||||
|
||||
static func logPremiumUpgradeTapped(source: String) {
|
||||
logEvent(AnalyticsEvent.premiumUpgradeTapped, parameters: ["source": source])
|
||||
}
|
||||
|
||||
static func logPremiumPurchased() {
|
||||
logEvent(AnalyticsEvent.premiumPurchased)
|
||||
}
|
||||
|
||||
static func logPremiumRestored() {
|
||||
logEvent(AnalyticsEvent.premiumRestored)
|
||||
}
|
||||
|
||||
static func logOnboardingCompleted() {
|
||||
logEvent(AnalyticsEvent.onboardingCompleted)
|
||||
}
|
||||
|
||||
static func logOnboardingStepViewed(step: Int) {
|
||||
logEvent(AnalyticsEvent.onboardingStepViewed, parameters: ["step": step])
|
||||
}
|
||||
|
||||
static func logAutoAssignUsed(filledSlots: Int, unfilledSlots: Int) {
|
||||
logEvent(AnalyticsEvent.autoAssignUsed, parameters: [
|
||||
"filled_slots": filledSlots,
|
||||
"unfilled_slots": unfilledSlots
|
||||
])
|
||||
}
|
||||
|
||||
static func logICloudSyncToggled(enabled: Bool) {
|
||||
logEvent(AnalyticsEvent.iCloudSyncToggled, parameters: ["enabled": enabled])
|
||||
}
|
||||
|
||||
static func logNotificationsToggled(enabled: Bool) {
|
||||
logEvent(AnalyticsEvent.notificationsToggled, parameters: ["enabled": enabled])
|
||||
}
|
||||
}
|
||||
@@ -95,14 +95,10 @@ struct AutocompleteEngine {
|
||||
plan: WeekPlan,
|
||||
tagMap: [UUID: Tag]
|
||||
) -> Bool {
|
||||
// Default behavior: if a dish has no explicit rules, avoid repeating it in the same week.
|
||||
if !dishHasExplicitRules(dish: dish, tagMap: tagMap) {
|
||||
let alreadyAssignedThisWeek = plan.slots.contains {
|
||||
$0.id != slot.id && $0.dishId == dish.id
|
||||
}
|
||||
if alreadyAssignedThisWeek { return true }
|
||||
}
|
||||
return false
|
||||
// Default behavior: avoid assigning the same dish twice in the same week,
|
||||
// regardless of whether it has explicit tag rules. The fallback path in
|
||||
// autocomplete() still allows repeating when no alternative exists.
|
||||
return plan.slots.contains { $0.id != slot.id && $0.dishId == dish.id }
|
||||
}
|
||||
|
||||
private static func violatesExplicitRules(
|
||||
@@ -149,19 +145,6 @@ struct AutocompleteEngine {
|
||||
return false
|
||||
}
|
||||
|
||||
private static func dishHasExplicitRules(dish: Dish, tagMap: [UUID: Tag]) -> Bool {
|
||||
for tagId in dish.tagIds {
|
||||
guard let tag = tagMap[tagId] else { continue }
|
||||
if tag.maxPerWeek != nil ||
|
||||
tag.noConsecutive ||
|
||||
tag.noDuplicateInDay ||
|
||||
tag.mealTypeRestriction != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static func adjacentSlots(of slot: MealSlot, in plan: WeekPlan) -> [MealSlot] {
|
||||
var result: [MealSlot] = []
|
||||
if slot.dayOfWeek > 0,
|
||||
|
||||
@@ -60,6 +60,50 @@ final class CalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
func updateEvent(
|
||||
eventId: String,
|
||||
slot: MealSlot,
|
||||
dishName: String,
|
||||
dishDescription: String?,
|
||||
weekStartDate: Date,
|
||||
settings: AppSettings
|
||||
) -> String? {
|
||||
guard let event = eventStore.event(withIdentifier: eventId) else {
|
||||
// Event was deleted externally — create a fresh one
|
||||
return createEvent(
|
||||
slot: slot,
|
||||
dishName: dishName,
|
||||
dishDescription: dishDescription,
|
||||
weekStartDate: weekStartDate,
|
||||
settings: settings
|
||||
)
|
||||
}
|
||||
|
||||
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
|
||||
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
|
||||
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
|
||||
event.title = "\(prefix)\(mealName): \(dishName)"
|
||||
|
||||
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
||||
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
||||
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
|
||||
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
||||
event.notes = dishDescription
|
||||
|
||||
event.alarms?.forEach { event.removeAlarm($0) }
|
||||
if let reminder = settings.reminderMinutesBefore {
|
||||
event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60)))
|
||||
}
|
||||
|
||||
do {
|
||||
try eventStore.save(event, span: .thisEvent)
|
||||
return event.eventIdentifier
|
||||
} catch {
|
||||
print("Error updating event: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteEvent(eventId: String) {
|
||||
guard let event = eventStore.event(withIdentifier: eventId) else { return }
|
||||
do {
|
||||
|
||||
@@ -47,6 +47,7 @@ final class DishViewModel: ObservableObject {
|
||||
tagIds: Array(selectedTagIds)
|
||||
)
|
||||
context.insert(dish)
|
||||
AnalyticsService.logDishAdded(tagCount: selectedTagIds.count)
|
||||
}
|
||||
|
||||
try? context.save()
|
||||
@@ -63,6 +64,7 @@ final class DishViewModel: ObservableObject {
|
||||
|
||||
context.delete(dish)
|
||||
try? context.save()
|
||||
AnalyticsService.logDishDeleted()
|
||||
reset()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ final class HomeViewModel: ObservableObject {
|
||||
plan.updatedAt = Date()
|
||||
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings)
|
||||
AnalyticsService.logMealAssigned()
|
||||
|
||||
HapticManager.shared.notification(type: .success)
|
||||
showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
|
||||
@@ -107,6 +108,7 @@ final class HomeViewModel: ObservableObject {
|
||||
slot.isRuleOverridden = false
|
||||
plan.updatedAt = Date()
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
|
||||
AnalyticsService.logMealCleared()
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
}
|
||||
|
||||
@@ -124,6 +126,7 @@ final class HomeViewModel: ObservableObject {
|
||||
|
||||
plan.updatedAt = Date()
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
|
||||
AnalyticsService.logAutoAssignUsed(filledSlots: result.filledSlots.count, unfilledSlots: result.unfilledCount)
|
||||
|
||||
Task {
|
||||
for _ in result.filledSlots {
|
||||
@@ -159,6 +162,7 @@ final class HomeViewModel: ObservableObject {
|
||||
slot.isRuleOverridden = false
|
||||
}
|
||||
plan.updatedAt = Date()
|
||||
AnalyticsService.logWeekReset()
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
showToastMessage(localizedString("toast_week_reset", language: settings.languageEnum.resolved()))
|
||||
}
|
||||
@@ -378,21 +382,34 @@ final class HomeViewModel: ObservableObject {
|
||||
private func syncAllAssignedSlotsToCalendar(plan: WeekPlan, dishes: [Dish], settings: AppSettings) {
|
||||
let dishById = Dictionary(uniqueKeysWithValues: dishes.map { ($0.id, $0) })
|
||||
for slot in plan.slots {
|
||||
if let oldEventId = slot.calendarEventId {
|
||||
CalendarService.shared.deleteEvent(eventId: oldEventId)
|
||||
guard let dishId = slot.dishId, let dish = dishById[dishId] else {
|
||||
if let eventId = slot.calendarEventId {
|
||||
CalendarService.shared.deleteEvent(eventId: eventId)
|
||||
slot.calendarEventId = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
guard let dishId = slot.dishId, let dish = dishById[dishId] else { continue }
|
||||
if let existingEventId = slot.calendarEventId {
|
||||
slot.calendarEventId = CalendarService.shared.updateEvent(
|
||||
eventId: existingEventId,
|
||||
slot: slot,
|
||||
dishName: dish.name,
|
||||
dishDescription: dish.descriptionText,
|
||||
weekStartDate: plan.weekStartDate,
|
||||
settings: settings
|
||||
)
|
||||
} else {
|
||||
slot.calendarEventId = CalendarService.shared.createEvent(
|
||||
slot: slot,
|
||||
dishName: dish.name,
|
||||
dishDescription: dish.descriptionText,
|
||||
weekStartDate: currentWeekStart,
|
||||
weekStartDate: plan.weekStartDate,
|
||||
settings: settings
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRuleOverrideFlag(for slot: MealSlot, plan: WeekPlan, allTags: [Tag], allDishes: [Dish]) {
|
||||
guard let dishId = slot.dishId, let dish = allDishes.first(where: { $0.id == dishId }) else {
|
||||
|
||||
@@ -35,7 +35,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
case 1: return true // Meal windows (always has selection)
|
||||
case 2: return true // Weekends (toggle)
|
||||
case 3: return true // Calendar (optional)
|
||||
case 4: return addedDishes.count >= 2 // Need at least 2 dishes
|
||||
case 4: return true // Dishes are optional
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
AnalyticsService.logOnboardingCompleted()
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingAutoAssignPromptKey)
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
||||
return true
|
||||
|
||||
@@ -10,6 +10,7 @@ struct DishFormView: View {
|
||||
@StateObject private var viewModel = DishViewModel()
|
||||
@State private var showPremium = false
|
||||
@State private var showDishLimitUpsell = false
|
||||
@State private var showSavedFeedback = false
|
||||
|
||||
var dish: Dish?
|
||||
|
||||
@@ -30,6 +31,20 @@ struct DishFormView: View {
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
if showSavedFeedback {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
Text("toast_dish_saved")
|
||||
}
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodSuccess)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.mealMoodSuccess.opacity(0.1))
|
||||
.cornerRadius(10)
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
|
||||
// Name field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("dish_name_label")
|
||||
@@ -150,7 +165,7 @@ struct DishFormView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("dish_cancel") { dismiss() }
|
||||
Button(viewModel.isEditing ? "dish_cancel" : "dish_done") { dismiss() }
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -163,8 +178,17 @@ struct DishFormView: View {
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
return
|
||||
}
|
||||
let wasEditing = viewModel.isEditing
|
||||
viewModel.save(context: context)
|
||||
if wasEditing {
|
||||
dismiss()
|
||||
} else {
|
||||
withAnimation { showSavedFeedback = true }
|
||||
HapticManager.shared.notification(type: .success)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
withAnimation { showSavedFeedback = false }
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "checkmark")
|
||||
.fontWeight(.semibold)
|
||||
|
||||
@@ -51,6 +51,13 @@ struct FirstDishesStepView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Text("first_dishes_optional_hint")
|
||||
.font(.mealMoodCaption)
|
||||
.foregroundColor(.mealMoodCoral.opacity(0.8))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, -16)
|
||||
|
||||
// New dish form
|
||||
VStack(spacing: 12) {
|
||||
TextField("first_dishes_name_placeholder", text: $viewModel.newDishName)
|
||||
@@ -232,7 +239,7 @@ struct FirstDishesStepView: View {
|
||||
HapticManager.shared.notification(type: .success)
|
||||
onFinish()
|
||||
},
|
||||
isEnabled: viewModel.addedDishes.count >= 2
|
||||
isEnabled: true
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 12)
|
||||
|
||||
@@ -79,6 +79,10 @@ struct OnboardingView: View {
|
||||
.onAppear {
|
||||
ensureDefaultTagsIfNeeded()
|
||||
Task { await restoreFromICloudAndFinishIfNeeded() }
|
||||
AnalyticsService.logOnboardingStepViewed(step: 0)
|
||||
}
|
||||
.onChange(of: viewModel.currentStep) { _, newStep in
|
||||
AnalyticsService.logOnboardingStepViewed(step: newStep)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
default_platform(:ios)
|
||||
|
||||
# Credentials are managed via `pass` (GPG + Gitea).
|
||||
# Usage: pass show apple/mealmood/<key>
|
||||
# Required before running lanes:
|
||||
# export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=$(pass show apple/mealmood/app-specific-password-fastlane)
|
||||
|
||||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
lane :beta do
|
||||
increment_build_number(xcodeproj: "MealMood.xcodeproj")
|
||||
build_app(
|
||||
scheme: "MealMood",
|
||||
export_method: "app-store",
|
||||
sdk: "iphoneos26.4",
|
||||
export_xcargs: "-allowProvisioningUpdates"
|
||||
)
|
||||
upload_to_testflight(skip_waiting_for_build_processing: true)
|
||||
end
|
||||
|
||||
desc "Push a new release build to the App Store"
|
||||
lane :release do
|
||||
increment_build_number(xcodeproj: "MealMood.xcodeproj")
|
||||
build_app(
|
||||
scheme: "MealMood",
|
||||
sdk: "iphoneos26.4",
|
||||
export_xcargs: "-allowProvisioningUpdates"
|
||||
)
|
||||
upload_to_app_store(force: true)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
Intelligentere Planung in diesem Update:
|
||||
|
||||
• Die Auto-Vervollständigung vermeidet jetzt die Wiederholung desselben Gerichts, wenn Alternativen verfügbar sind — abwechslungsreichere und ausgewogenere Wochen.
|
||||
• Mehrere Gerichte hinzufügen geht jetzt schneller: Das Formular bleibt nach dem Speichern offen, damit du mehrere hintereinander hinzufügen kannst.
|
||||
• Kalendersynchronisation korrigiert: Ereignisse werden jetzt aktualisiert statt dupliziert, wenn erneut synchronisiert wird.
|
||||
• Einrichtung vereinfacht: Gerichte sind beim ersten Setup jetzt optional — du kannst sie später hinzufügen, wenn du möchtest.
|
||||
|
||||
Allgemeine Verbesserungen und Fehlerbehebungen.
|
||||
@@ -0,0 +1,8 @@
|
||||
Smarter meal planning in this update:
|
||||
|
||||
• Auto-assign now avoids repeating the same dish twice when alternatives are available — smarter, more varied weekly plans.
|
||||
• Adding multiple dishes is now faster: the form stays open after saving so you can add several in a row.
|
||||
• Calendar sync fixed: events are now updated instead of duplicated when re-syncing.
|
||||
• Onboarding simplified: dishes are now optional during setup — jump in and add them later if you prefer.
|
||||
|
||||
Various improvements and bug fixes.
|
||||
@@ -0,0 +1,8 @@
|
||||
Planificación más inteligente en esta actualización:
|
||||
|
||||
• El auto-completar ahora evita repetir el mismo plato dos veces si hay alternativas — semanas más variadas y equilibradas.
|
||||
• Añadir varios platos es más rápido: el formulario se queda abierto tras guardar para poder añadir varios seguidos.
|
||||
• Sincronización con el calendario corregida: los eventos ahora se actualizan en lugar de duplicarse al volver a sincronizar.
|
||||
• Incorporación simplificada: los platos ahora son opcionales durante la configuración inicial — puedes añadirlos después si lo prefieres.
|
||||
|
||||
Mejoras generales y corrección de errores.
|
||||
@@ -0,0 +1,8 @@
|
||||
Une planification plus intelligente dans cette mise à jour :
|
||||
|
||||
• L'auto-complétion évite désormais de répéter le même plat deux fois lorsqu'il y a des alternatives — des semaines plus variées et équilibrées.
|
||||
• Ajouter plusieurs plats est plus rapide : le formulaire reste ouvert après l'enregistrement pour en ajouter plusieurs à la suite.
|
||||
• Synchronisation du calendrier corrigée : les événements sont maintenant mis à jour au lieu d'être dupliqués lors d'une nouvelle synchronisation.
|
||||
• Démarrage simplifié : les plats sont désormais optionnels pendant la configuration initiale — vous pouvez les ajouter plus tard si vous préférez.
|
||||
|
||||
Améliorations générales et corrections de bugs.
|
||||
@@ -0,0 +1,8 @@
|
||||
Pianificazione più intelligente in questo aggiornamento:
|
||||
|
||||
• Il completamento automatico ora evita di ripetere lo stesso piatto due volte quando ci sono alternative — settimane più varie ed equilibrate.
|
||||
• Aggiungere più piatti è più veloce: il modulo rimane aperto dopo il salvataggio per poterne aggiungere più di uno di seguito.
|
||||
• Sincronizzazione del calendario corretta: gli eventi vengono ora aggiornati invece di essere duplicati quando si risincronizza.
|
||||
• Configurazione semplificata: i piatti sono ora opzionali durante la configurazione iniziale — puoi aggiungerli dopo se preferisci.
|
||||
|
||||
Miglioramenti generali e correzioni di bug.
|
||||
@@ -0,0 +1,8 @@
|
||||
Planejamento mais inteligente nesta atualização:
|
||||
|
||||
• O preenchimento automático agora evita repetir o mesmo prato duas vezes quando há alternativas disponíveis — semanas mais variadas e equilibradas.
|
||||
• Adicionar vários pratos é mais rápido: o formulário permanece aberto após salvar para adicionar vários seguidos.
|
||||
• Sincronização com o calendário corrigida: os eventos agora são atualizados em vez de duplicados ao sincronizar novamente.
|
||||
• Configuração simplificada: os pratos agora são opcionais durante a configuração inicial — você pode adicioná-los depois, se preferir.
|
||||
|
||||
Melhorias gerais e correções de bugs.
|
||||
Reference in New Issue
Block a user