diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3ada835 --- /dev/null +++ b/CLAUDE.md @@ -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 | diff --git a/MealMood.xcodeproj/project.pbxproj b/MealMood.xcodeproj/project.pbxproj index bc7f8f2..bf144fc 100644 --- a/MealMood.xcodeproj/project.pbxproj +++ b/MealMood.xcodeproj/project.pbxproj @@ -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 = ""; }; 0164A33496A304E40685EA6D /* Localization+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Helpers.swift"; sourceTree = ""; }; @@ -97,6 +124,7 @@ 168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = ""; }; 17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagLocalizationTests.swift; sourceTree = ""; }; 17E00AB23342224B3407838B /* DefaultDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDataService.swift; sourceTree = ""; }; + 1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SocialShareSheet.swift; sourceTree = ""; }; 20F23BF9788A3217E3D63721 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; 23957C735075B0DFB1DF51D7 /* DishDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishDrawerView.swift; sourceTree = ""; }; 2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealMoodApp.swift; sourceTree = ""; }; @@ -106,9 +134,12 @@ 4EDEE086FD20D16DE507B247 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 50E1C12C3A693D7C41E9F80B /* TagPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPill.swift; sourceTree = ""; }; 5187023D83BBC5E8D304CE3A /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + 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 = ""; }; + 5D6A3FCADC4F4B7AAB73E5CA /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60F0A5C2FF1B797A4BB69B39 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 623EDDC4CD134883FC9D242D /* Dish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dish.swift; sourceTree = ""; }; + 62E2F6B2694CEC97AE3C1B13 /* PremiumSyncServiceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PremiumSyncServiceTests.swift; sourceTree = ""; }; 69B309A43E486B5A723324BA /* WeekCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekCalendarView.swift; sourceTree = ""; }; 6D718B2E4F7E6D22E502F0A6 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 6EA1948B5DAEEEA09B78C8A9 /* SecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryButton.swift; sourceTree = ""; }; @@ -122,18 +153,25 @@ 89C47DD7B8F4B1441780E611 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumUpsellBanner.swift; sourceTree = ""; }; 8EE10285FEE9BE0684CC66A3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PremiumSyncService.swift; sourceTree = ""; }; 93B4B31A8F6E4D7AAB1B6A01 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 965555A7774FF7EB975F0A07 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 9F86EB5DDB126D73486FD956 /* MealMood.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MealMood.entitlements; sourceTree = ""; }; 9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealWindowsStepView.swift; sourceTree = ""; }; A23267BAA116E1214870416C /* WeekPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekPlan.swift; sourceTree = ""; }; + A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = ""; }; AB771E85924AE086647A5113 /* Color+MealMood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+MealMood.swift"; sourceTree = ""; }; + B042BB2F81973D854E84BABD /* MealMoodWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MealMoodWidget.swift; sourceTree = ""; }; B0A631C6D0CF7A0724ACF6A5 /* CalendarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarService.swift; sourceTree = ""; }; + B3369706BF941E09F88243F1 /* MealMoodWidget.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = MealMoodWidget.entitlements; sourceTree = ""; }; B3944B4AD876455057EB2265 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B68E5C82608F60FF8798BB81 /* EmptySlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySlotView.swift; sourceTree = ""; }; B6CF1C9CC9BDB3B20B1834EB /* TagSelectorSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectorSheet.swift; sourceTree = ""; }; BAC64110469AA91D5F28C423 /* MealMoodLogoMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealMoodLogoMark.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; CB3BD0E078644749E93B0D2B /* DishListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishListView.swift; sourceTree = ""; }; @@ -147,8 +185,11 @@ E18A2F6B56D64C1EBC91E21B /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; E8472961AE65E2A3460B67C3 /* DishViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishViewModel.swift; sourceTree = ""; }; E8825212ACFA0D3F6C716E94 /* ReviewPromptService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptService.swift; sourceTree = ""; }; + 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 = ""; }; - 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 = ""; }; + FB0011111111111111111107 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; FC01EE9D78D980727A644CDF /* View+MealCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+MealCard.swift"; sourceTree = ""; }; FD4FD72583C004A5D5C939A3 /* FilledSlotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilledSlotView.swift; sourceTree = ""; }; FD621CE9B44512AE8894D92E /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; @@ -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 = ""; }; @@ -184,10 +239,21 @@ 89C47DD7B8F4B1441780E611 /* HomeView.swift */, 69B309A43E486B5A723324BA /* WeekCalendarView.swift */, 2E113A29DEC0551C41AB7EBB /* WeekPlanShareView.swift */, + 1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */, ); path = Home; sourceTree = ""; }; + 2BC91A087BC03FF7810C529F /* MealMoodWidget */ = { + isa = PBXGroup; + children = ( + B042BB2F81973D854E84BABD /* MealMoodWidget.swift */, + 5D6A3FCADC4F4B7AAB73E5CA /* Info.plist */, + B3369706BF941E09F88243F1 /* MealMoodWidget.entitlements */, + ); + path = MealMoodWidget; + sourceTree = ""; + }; 35F15C6817E504B1114420CD /* Premium */ = { isa = PBXGroup; children = ( @@ -261,6 +327,7 @@ children = ( C7B13F4D4D362E50CA65A0C1 /* MealMood.app */, F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */, + 574CB4BDE993DA0CEC251ABD /* MealMoodWidget.appex */, ); name = Products; sourceTree = ""; @@ -272,6 +339,7 @@ DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */, 168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */, 17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */, + 62E2F6B2694CEC97AE3C1B13 /* PremiumSyncServiceTests.swift */, ); path = MealMoodTests; sourceTree = ""; @@ -324,6 +392,7 @@ 8E15828872FAEA71A7384099 /* Localizable.strings */, 9F86EB5DDB126D73486FD956 /* MealMood.entitlements */, 4ED1758EE5CD0A3EB9D9BCDF /* Brand */, + FB0011111111111111111103 /* GoogleService-Info.plist */, ); path = Resources; sourceTree = ""; @@ -367,10 +436,30 @@ path = Components; sourceTree = ""; }; + ECCF14900EFD2F0E1432DB05 /* iOS */ = { + isa = PBXGroup; + children = ( + C77C350D4A839B76A3D2EFE7 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + F4FE25386665429412E51DB4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ECCF14900EFD2F0E1432DB05 /* iOS */, + BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */, + EB4FC89C90928DD833240244 /* WidgetKit.framework */, + C2D1AAC0DB21EA5E2CCA38AD /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 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 = ""; @@ -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 */; diff --git a/MealMood/Resources/Info.plist b/MealMood/Resources/Info.plist index bd02d56..bc5d7e3 100644 --- a/MealMood/Resources/Info.plist +++ b/MealMood/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - $(MARKETING_VERSION) + 1.0.4 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 21 GADApplicationIdentifier ca-app-pub-1549720748100858~9985112590 LSRequiresIPhoneOS diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings index 0b6ab12..9b70cb8 100644 --- a/MealMood/Resources/de.lproj/Localizable.strings +++ b/MealMood/Resources/de.lproj/Localizable.strings @@ -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 ✨"; diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index a201b0c..e4ec220 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -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 ✨"; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index 3df7935..554484a 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -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í ✨"; diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings index 146c663..fdfd876 100644 --- a/MealMood/Resources/fr.lproj/Localizable.strings +++ b/MealMood/Resources/fr.lproj/Localizable.strings @@ -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 ✨"; diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings index 82e4f69..20adf17 100644 --- a/MealMood/Resources/it.lproj/Localizable.strings +++ b/MealMood/Resources/it.lproj/Localizable.strings @@ -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ì ✨"; diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings index 6ad6ba6..ed9764b 100644 --- a/MealMood/Resources/pt-BR.lproj/Localizable.strings +++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings @@ -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 ✨"; diff --git a/MealMood/Services/AnalyticsService.swift b/MealMood/Services/AnalyticsService.swift new file mode 100644 index 0000000..dc407c8 --- /dev/null +++ b/MealMood/Services/AnalyticsService.swift @@ -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]) + } +} diff --git a/MealMood/Services/AutocompleteEngine.swift b/MealMood/Services/AutocompleteEngine.swift index 1daffee..dee011c 100644 --- a/MealMood/Services/AutocompleteEngine.swift +++ b/MealMood/Services/AutocompleteEngine.swift @@ -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, diff --git a/MealMood/Services/CalendarService.swift b/MealMood/Services/CalendarService.swift index 57a54b4..d4c8629 100644 --- a/MealMood/Services/CalendarService.swift +++ b/MealMood/Services/CalendarService.swift @@ -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 { diff --git a/MealMood/ViewModels/DishViewModel.swift b/MealMood/ViewModels/DishViewModel.swift index 5c0e7b7..4c345da 100644 --- a/MealMood/ViewModels/DishViewModel.swift +++ b/MealMood/ViewModels/DishViewModel.swift @@ -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 } diff --git a/MealMood/ViewModels/HomeViewModel.swift b/MealMood/ViewModels/HomeViewModel.swift index c33315b..5bd47e2 100644 --- a/MealMood/ViewModels/HomeViewModel.swift +++ b/MealMood/ViewModels/HomeViewModel.swift @@ -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,19 +382,32 @@ 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) - slot.calendarEventId = nil + 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 } - slot.calendarEventId = CalendarService.shared.createEvent( - slot: slot, - dishName: dish.name, - dishDescription: dish.descriptionText, - weekStartDate: currentWeekStart, - settings: settings - ) + 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: plan.weekStartDate, + settings: settings + ) + } } } diff --git a/MealMood/ViewModels/OnboardingViewModel.swift b/MealMood/ViewModels/OnboardingViewModel.swift index e9d39b4..fab1ee2 100644 --- a/MealMood/ViewModels/OnboardingViewModel.swift +++ b/MealMood/ViewModels/OnboardingViewModel.swift @@ -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 diff --git a/MealMood/Views/Dishes/DishFormView.swift b/MealMood/Views/Dishes/DishFormView.swift index 0e4ea86..623d529 100644 --- a/MealMood/Views/Dishes/DishFormView.swift +++ b/MealMood/Views/Dishes/DishFormView.swift @@ -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) - dismiss() + 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) diff --git a/MealMood/Views/Onboarding/FirstDishesStepView.swift b/MealMood/Views/Onboarding/FirstDishesStepView.swift index e9fa3e6..15dd422 100644 --- a/MealMood/Views/Onboarding/FirstDishesStepView.swift +++ b/MealMood/Views/Onboarding/FirstDishesStepView.swift @@ -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) diff --git a/MealMood/Views/Onboarding/OnboardingView.swift b/MealMood/Views/Onboarding/OnboardingView.swift index 19528d6..dd5bb5e 100644 --- a/MealMood/Views/Onboarding/OnboardingView.swift +++ b/MealMood/Views/Onboarding/OnboardingView.swift @@ -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) } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..550deb9 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,31 @@ +default_platform(:ios) + +# Credentials are managed via `pass` (GPG + Gitea). +# Usage: pass show apple/mealmood/ +# 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 diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt new file mode 100644 index 0000000..dc1b0c4 --- /dev/null +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -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. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt new file mode 100644 index 0000000..8374569 --- /dev/null +++ b/fastlane/metadata/en-US/release_notes.txt @@ -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. diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt new file mode 100644 index 0000000..0a04c24 --- /dev/null +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -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. diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt new file mode 100644 index 0000000..9db5542 --- /dev/null +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -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. diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt new file mode 100644 index 0000000..08ee749 --- /dev/null +++ b/fastlane/metadata/it/release_notes.txt @@ -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. diff --git a/fastlane/metadata/pt-BR/release_notes.txt b/fastlane/metadata/pt-BR/release_notes.txt new file mode 100644 index 0000000..786ed49 --- /dev/null +++ b/fastlane/metadata/pt-BR/release_notes.txt @@ -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.