1.2.0: contextual notifications + in-onboarding auto-fill (aha moment)
- New "Week Ready" onboarding step: auto-fills the first week inline right after dish creation, then pitches the Sunday reminder with context before requesting OS notification permission (no more cold prompt on Home) - Notifications section in Settings: weekly reminder toggle, deep-link to system settings when permission is denied - NotificationService: remindersEnabled preference, requestPermission(), authorizationStatus(), cancelAllReminders() - Post-onboarding premium prompt deferred to 2nd session (paywall was just shown as the final onboarding step — 2s-later alert was prompt fatigue) - Back navigation blocked after onboarding commit to avoid duplicate dishes - 10 new strings × 6 languages Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtWT52pAE91D7mbDda1cAM
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
17E1054EE183486E7BE38C19 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */; };
|
17E1054EE183486E7BE38C19 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */; };
|
||||||
19330C6ECE7A60110A3A679D /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB00DA865F6FE86952BFC3 /* TagListView.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 */; };
|
203DDFF14C0688DEDB6EE83E /* Localization+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0164A33496A304E40685EA6D /* Localization+Helpers.swift */; };
|
||||||
|
2124FD777418A4EEDF64BDC3 /* WeekReadyStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D954415A7AC73D41309D2BDF /* WeekReadyStepView.swift */; };
|
||||||
233B9C1A5CA7E938318491C0 /* SocialShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1975C49FFC7D91AEB7932C97 /* SocialShareSheet.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 */; };
|
2519A55C6BDA686B6F85E79D /* View+MealCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC01EE9D78D980727A644CDF /* View+MealCard.swift */; };
|
||||||
25B3C328E088A403CABAF20A /* PremiumSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */; };
|
25B3C328E088A403CABAF20A /* PremiumSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928E6D4F6D78AD1840245FB4 /* PremiumSyncService.swift */; };
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
37F5CEC4862C0AE2F3385ABA /* DishListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BD0E078644749E93B0D2B /* DishListView.swift */; };
|
37F5CEC4862C0AE2F3385ABA /* DishListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BD0E078644749E93B0D2B /* DishListView.swift */; };
|
||||||
3C5225918DA8DE77F5AC1AA0 /* MealMoodApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */; };
|
3C5225918DA8DE77F5AC1AA0 /* MealMoodApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */; };
|
||||||
4218F220B7412965A5001A88 /* CalendarStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76719058DD3A4177B2B4163E /* CalendarStepView.swift */; };
|
4218F220B7412965A5001A88 /* CalendarStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76719058DD3A4177B2B4163E /* CalendarStepView.swift */; };
|
||||||
|
443CC3997A9A4818B51066FB /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AD94FC874467D863D464C /* StatsView.swift */; };
|
||||||
478AE18DA0F887D3D5164635 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C77C350D4A839B76A3D2EFE7 /* Foundation.framework */; };
|
478AE18DA0F887D3D5164635 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C77C350D4A839B76A3D2EFE7 /* Foundation.framework */; };
|
||||||
48F9D3308375F820B2B88D1B /* PremiumUpsellBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */; };
|
48F9D3308375F820B2B88D1B /* PremiumUpsellBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFA001E1BFA916ABC4C44C1 /* PremiumUpsellBanner.swift */; };
|
||||||
4D53B5101AE845D7C03A0EB4 /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = 192E7CF40FD34C3FF4DDFDA3 /* GoogleMobileAds */; };
|
4D53B5101AE845D7C03A0EB4 /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = 192E7CF40FD34C3FF4DDFDA3 /* GoogleMobileAds */; };
|
||||||
@@ -61,12 +63,13 @@
|
|||||||
ACAEFA5F4C170C6438CD2652 /* TagLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */; };
|
ACAEFA5F4C170C6438CD2652 /* TagLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B976BF8B0BD6D1AA5F8C68 /* TagLocalizationTests.swift */; };
|
||||||
AD5F8FDE30CE4B2934B3C1C1 /* WeekPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23267BAA116E1214870416C /* WeekPlan.swift */; };
|
AD5F8FDE30CE4B2934B3C1C1 /* WeekPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23267BAA116E1214870416C /* WeekPlan.swift */; };
|
||||||
B3C9E5149B70C477EACBDFFB /* AdBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050EA827CA951AB9E847E2CA /* AdBannerView.swift */; };
|
B3C9E5149B70C477EACBDFFB /* AdBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050EA827CA951AB9E847E2CA /* AdBannerView.swift */; };
|
||||||
C7B1F5A21B5E4A9E91B7F5A2 /* BottomPromoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E2F6A3F47E5C9F92A8E6B3 /* BottomPromoBanner.swift */; };
|
B4D6F8012C3E5F7A91B3D5F7 /* FeedbackStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C5F7891B2D4E6F80A2C4E6 /* FeedbackStore.swift */; };
|
||||||
B8D7CA62789429C2F693E4F6 /* MealMoodLogoMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC64110469AA91D5F28C423 /* MealMoodLogoMark.swift */; };
|
B8D7CA62789429C2F693E4F6 /* MealMoodLogoMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC64110469AA91D5F28C423 /* MealMoodLogoMark.swift */; };
|
||||||
BE908B23F64F33D594AB8D82 /* DateHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */; };
|
BE908B23F64F33D594AB8D82 /* DateHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */; };
|
||||||
C14BDE4C3EF6AF75F378D892 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */; };
|
C14BDE4C3EF6AF75F378D892 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */; };
|
||||||
C3CB4ACC213DAAD25E610C56 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB4FC89C90928DD833240244 /* WidgetKit.framework */; };
|
C3CB4ACC213DAAD25E610C56 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB4FC89C90928DD833240244 /* WidgetKit.framework */; };
|
||||||
C565A1975302748A2B2152BE /* DishViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8472961AE65E2A3460B67C3 /* DishViewModel.swift */; };
|
C565A1975302748A2B2152BE /* DishViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8472961AE65E2A3460B67C3 /* DishViewModel.swift */; };
|
||||||
|
C7B1F5A21B5E4A9E91B7F5A2 /* BottomPromoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E2F6A3F47E5C9F92A8E6B3 /* BottomPromoBanner.swift */; };
|
||||||
C81C935386BE1E939DA55F8D /* MealWindowsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */; };
|
C81C935386BE1E939DA55F8D /* MealWindowsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA11C8A81D5C0CA8A3C13D /* MealWindowsStepView.swift */; };
|
||||||
CD174669421FE1FD4315F6E0 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */; };
|
CD174669421FE1FD4315F6E0 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB0D740B79B9E129A3C2F382 /* WidgetKit.framework */; };
|
||||||
CD2ADCDADFEC2D562E23D8E6 /* AutocompleteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */; };
|
CD2ADCDADFEC2D562E23D8E6 /* AutocompleteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */; };
|
||||||
@@ -74,11 +77,11 @@
|
|||||||
D8149D55E74F7BBFC0402F46 /* DishFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */; };
|
D8149D55E74F7BBFC0402F46 /* DishFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */; };
|
||||||
D89413095E37C3D677856437 /* PremiumAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0200C0C11C912711C724558 /* PremiumAccess.swift */; };
|
D89413095E37C3D677856437 /* PremiumAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0200C0C11C912711C724558 /* PremiumAccess.swift */; };
|
||||||
DE43F08AAC939C083BF511C1 /* FirstDishesStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10594EA55F6BEFF4241D364C /* FirstDishesStepView.swift */; };
|
DE43F08AAC939C083BF511C1 /* FirstDishesStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10594EA55F6BEFF4241D364C /* FirstDishesStepView.swift */; };
|
||||||
E5D9C4B783A14E2B9F4D2E68 /* PaywallStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */; };
|
|
||||||
DE797C90DFDAFAC5C12E3D97 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBCD4FFE0D067A591104B74 /* ToastView.swift */; };
|
DE797C90DFDAFAC5C12E3D97 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBCD4FFE0D067A591104B74 /* ToastView.swift */; };
|
||||||
E12A5EE3D940260902277CC5 /* WeekCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B309A43E486B5A723324BA /* WeekCalendarView.swift */; };
|
E12A5EE3D940260902277CC5 /* WeekCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B309A43E486B5A723324BA /* WeekCalendarView.swift */; };
|
||||||
E2524335ED3A5DB7813BEA99 /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D140A6E835CA6227C2D4D9 /* WelcomeStepView.swift */; };
|
E2524335ED3A5DB7813BEA99 /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D140A6E835CA6227C2D4D9 /* WelcomeStepView.swift */; };
|
||||||
E3BD64E6D32ED7052F174923 /* TagSelectorSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF1C9CC9BDB3B20B1834EB /* TagSelectorSheet.swift */; };
|
E3BD64E6D32ED7052F174923 /* TagSelectorSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF1C9CC9BDB3B20B1834EB /* TagSelectorSheet.swift */; };
|
||||||
|
E5D9C4B783A14E2B9F4D2E68 /* PaywallStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */; };
|
||||||
E6A57F29C02AE138D705BAF7 /* AppIconPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F2EE5A756C183B6D505403 /* AppIconPlaceholder.swift */; };
|
E6A57F29C02AE138D705BAF7 /* AppIconPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F2EE5A756C183B6D505403 /* AppIconPlaceholder.swift */; };
|
||||||
EA4EC6244EEBE6D568BCA5E5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3944B4AD876455057EB2265 /* Assets.xcassets */; };
|
EA4EC6244EEBE6D568BCA5E5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3944B4AD876455057EB2265 /* Assets.xcassets */; };
|
||||||
F65AD80426AF8DA12FBD661C /* WeekendsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */; };
|
F65AD80426AF8DA12FBD661C /* WeekendsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */; };
|
||||||
@@ -87,8 +90,6 @@
|
|||||||
FB0011111111111111111106 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111107 /* AnalyticsService.swift */; };
|
FB0011111111111111111106 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0011111111111111111107 /* AnalyticsService.swift */; };
|
||||||
FB0011111111111111111108 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB001111111111111111110B /* FirebaseCrashlytics */; };
|
FB0011111111111111111108 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB001111111111111111110B /* FirebaseCrashlytics */; };
|
||||||
FB0011111111111111111109 /* CrashlyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB001111111111111111110A /* CrashlyticsService.swift */; };
|
FB0011111111111111111109 /* CrashlyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB001111111111111111110A /* CrashlyticsService.swift */; };
|
||||||
443CC3997A9A4818B51066FB /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AD94FC874467D863D464C /* StatsView.swift */; };
|
|
||||||
B4D6F8012C3E5F7A91B3D5F7 /* FeedbackStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C5F7891B2D4E6F80A2C4E6 /* FeedbackStore.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -121,18 +122,17 @@
|
|||||||
035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekendsStepView.swift; sourceTree = "<group>"; };
|
035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekendsStepView.swift; sourceTree = "<group>"; };
|
||||||
049AE68D5B577514ADD6BACB /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
049AE68D5B577514ADD6BACB /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||||
050EA827CA951AB9E847E2CA /* AdBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdBannerView.swift; sourceTree = "<group>"; };
|
050EA827CA951AB9E847E2CA /* AdBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdBannerView.swift; sourceTree = "<group>"; };
|
||||||
D8E2F6A3F47E5C9F92A8E6B3 /* BottomPromoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomPromoBanner.swift; sourceTree = "<group>"; };
|
|
||||||
065982B284538632951F1C3A /* MealMoodLogo.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = MealMoodLogo.svg; sourceTree = "<group>"; };
|
065982B284538632951F1C3A /* MealMoodLogo.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = MealMoodLogo.svg; sourceTree = "<group>"; };
|
||||||
0EC555FD2F4498780005513D /* aaaa.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = aaaa.storekit; sourceTree = "<group>"; };
|
0EC555FD2F4498780005513D /* aaaa.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = aaaa.storekit; sourceTree = "<group>"; };
|
||||||
0F46C32F97EAD52F8F3BC0B7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
0F46C32F97EAD52F8F3BC0B7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
103374A7D91B4F884278DA72 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = "<group>"; };
|
103374A7D91B4F884278DA72 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = "<group>"; };
|
||||||
10594EA55F6BEFF4241D364C /* FirstDishesStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstDishesStepView.swift; sourceTree = "<group>"; };
|
10594EA55F6BEFF4241D364C /* FirstDishesStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstDishesStepView.swift; sourceTree = "<group>"; };
|
||||||
F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallStepView.swift; sourceTree = "<group>"; };
|
|
||||||
13DB00DA865F6FE86952BFC3 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; };
|
13DB00DA865F6FE86952BFC3 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; };
|
||||||
168A6CA95246E1BB72797D4E /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
1975C49FFC7D91AEB7932C97 /* SocialShareSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SocialShareSheet.swift; sourceTree = "<group>"; };
|
||||||
|
1E8AD94FC874467D863D464C /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
||||||
20F23BF9788A3217E3D63721 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.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>"; };
|
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>"; };
|
2C20D9FC522FEBF7206CFD32 /* MealMoodApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealMoodApp.swift; sourceTree = "<group>"; };
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
9F86EB5DDB126D73486FD956 /* MealMood.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MealMood.entitlements; 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>"; };
|
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>"; };
|
A23267BAA116E1214870416C /* WeekPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekPlan.swift; sourceTree = "<group>"; };
|
||||||
|
A3C5F7891B2D4E6F80A2C4E6 /* FeedbackStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackStore.swift; sourceTree = "<group>"; };
|
||||||
A6FC2153490D53B80DBDC034 /* WidgetDataStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.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>"; };
|
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>"; };
|
B042BB2F81973D854E84BABD /* MealMoodWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MealMoodWidget.swift; sourceTree = "<group>"; };
|
||||||
@@ -186,6 +187,8 @@
|
|||||||
CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteEngine.swift; sourceTree = "<group>"; };
|
CE7AB2ECCA7D330E50AC549E /* AutocompleteEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteEngine.swift; sourceTree = "<group>"; };
|
||||||
CFBCD4FFE0D067A591104B74 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
CFBCD4FFE0D067A591104B74 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishFormView.swift; sourceTree = "<group>"; };
|
D63CA2A8666E1FBBAD65F141 /* DishFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DishFormView.swift; sourceTree = "<group>"; };
|
||||||
|
D8E2F6A3F47E5C9F92A8E6B3 /* BottomPromoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomPromoBanner.swift; sourceTree = "<group>"; };
|
||||||
|
D954415A7AC73D41309D2BDF /* WeekReadyStepView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WeekReadyStepView.swift; sourceTree = "<group>"; };
|
||||||
DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelpersTests.swift; sourceTree = "<group>"; };
|
DBD33453B7DF86EDBAEB80F1 /* DateHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelpersTests.swift; sourceTree = "<group>"; };
|
||||||
DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
|
DFA97AD958C922D8AFF73314 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
|
||||||
E18A2F6B56D64C1EBC91E219 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
E18A2F6B56D64C1EBC91E219 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
@@ -195,6 +198,7 @@
|
|||||||
E8825212ACFA0D3F6C716E94 /* ReviewPromptService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewPromptService.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; };
|
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>"; };
|
F0200C0C11C912711C724558 /* PremiumAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumAccess.swift; sourceTree = "<group>"; };
|
||||||
|
F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallStepView.swift; sourceTree = "<group>"; };
|
||||||
F766AA7898D6F391DE1B9A81 /* MealMoodTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; 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>"; };
|
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>"; };
|
FB0011111111111111111107 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||||
@@ -204,8 +208,6 @@
|
|||||||
FD621CE9B44512AE8894D92E /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
FD621CE9B44512AE8894D92E /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
||||||
FE73D5B7AF8B5E2D63105977 /* Font+MealMood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+MealMood.swift"; sourceTree = "<group>"; };
|
FE73D5B7AF8B5E2D63105977 /* Font+MealMood.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+MealMood.swift"; sourceTree = "<group>"; };
|
||||||
FF2DE77AD57DB6DAB188CD9E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
FF2DE77AD57DB6DAB188CD9E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
1E8AD94FC874467D863D464C /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
|
||||||
A3C5F7891B2D4E6F80A2C4E6 /* FeedbackStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackStore.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -256,6 +258,14 @@
|
|||||||
path = Home;
|
path = Home;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
0D63499EC79D43728D1CA8C7 /* Stats */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1E8AD94FC874467D863D464C /* StatsView.swift */,
|
||||||
|
);
|
||||||
|
path = Stats;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2BC91A087BC03FF7810C529F /* MealMoodWidget */ = {
|
2BC91A087BC03FF7810C529F /* MealMoodWidget */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -370,14 +380,6 @@
|
|||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
0D63499EC79D43728D1CA8C7 /* Stats */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
1E8AD94FC874467D863D464C /* StatsView.swift */,
|
|
||||||
);
|
|
||||||
path = Stats;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
8BE925662F55707DF083677A /* Dishes */ = {
|
8BE925662F55707DF083677A /* Dishes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -437,6 +439,7 @@
|
|||||||
F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */,
|
F61A8C92B47D5938C61E9A75 /* PaywallStepView.swift */,
|
||||||
035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */,
|
035C6C0B728EB0EA80394CDD /* WeekendsStepView.swift */,
|
||||||
70D140A6E835CA6227C2D4D9 /* WelcomeStepView.swift */,
|
70D140A6E835CA6227C2D4D9 /* WelcomeStepView.swift */,
|
||||||
|
D954415A7AC73D41309D2BDF /* WeekReadyStepView.swift */,
|
||||||
);
|
);
|
||||||
path = Onboarding;
|
path = Onboarding;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -621,6 +624,30 @@
|
|||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
FB001111111111111111110C /* Upload dSYM to Crashlytics */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}",
|
||||||
|
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
||||||
|
);
|
||||||
|
name = "Upload dSYM to Crashlytics";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
A6B405631E28429E34B68811 /* Sources */ = {
|
A6B405631E28429E34B68811 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
@@ -709,35 +736,12 @@
|
|||||||
FB0011111111111111111109 /* CrashlyticsService.swift in Sources */,
|
FB0011111111111111111109 /* CrashlyticsService.swift in Sources */,
|
||||||
443CC3997A9A4818B51066FB /* StatsView.swift in Sources */,
|
443CC3997A9A4818B51066FB /* StatsView.swift in Sources */,
|
||||||
B4D6F8012C3E5F7A91B3D5F7 /* FeedbackStore.swift in Sources */,
|
B4D6F8012C3E5F7A91B3D5F7 /* FeedbackStore.swift in Sources */,
|
||||||
|
2124FD777418A4EEDF64BDC3 /* WeekReadyStepView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
|
||||||
FB001111111111111111110C /* Upload dSYM to Crashlytics */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}",
|
|
||||||
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
|
||||||
);
|
|
||||||
name = "Upload dSYM to Crashlytics";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n";
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
C4E8F43173BDFCAE4D6DFF49 /* PBXTargetDependency */ = {
|
C4E8F43173BDFCAE4D6DFF49 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "Wie war diese Woche?";
|
"week_rating_prompt" = "Wie war diese Woche?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "Deine Woche ist fertig!";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d Mahlzeiten automatisch geplant. Du kannst später alles anpassen.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Dein Wochenplan ist eingerichtet. Füge jederzeit Gerichte hinzu und fülle ihn mit einem Tipp.";
|
||||||
|
"onboarding_reminder_pitch" = "Erhalte jeden Sonntag eine Erinnerung, damit die Woche immer geplant beginnt.";
|
||||||
|
"onboarding_reminder_allow" = "Sonntags erinnern";
|
||||||
|
"onboarding_reminder_skip" = "Jetzt nicht";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Mitteilungen";
|
||||||
|
"settings_weekly_reminder" = "Wöchentliche Planungserinnerung";
|
||||||
|
"settings_notifications_denied_hint" = "Mitteilungen sind in den Systemeinstellungen deaktiviert.";
|
||||||
|
"settings_open_system_settings" = "Einstellungen öffnen";
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "How was this week?";
|
"week_rating_prompt" = "How was this week?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "Your week is ready!";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d meals planned automatically. You can tweak anything later.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Your weekly plan is set up. Add dishes anytime and fill it in one tap.";
|
||||||
|
"onboarding_reminder_pitch" = "Get a gentle reminder every Sunday so next week is always planned before it starts.";
|
||||||
|
"onboarding_reminder_allow" = "Remind me on Sundays";
|
||||||
|
"onboarding_reminder_skip" = "Not now";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Notifications";
|
||||||
|
"settings_weekly_reminder" = "Weekly planning reminder";
|
||||||
|
"settings_notifications_denied_hint" = "Notifications are disabled in system settings.";
|
||||||
|
"settings_open_system_settings" = "Open Settings";
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "¿Qué tal fue la semana?";
|
"week_rating_prompt" = "¿Qué tal fue la semana?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "¡Tu semana está lista!";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d comidas planificadas automáticamente. Podrás ajustarlo todo después.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Tu plan semanal está preparado. Añade platos cuando quieras y complétalo con un toque.";
|
||||||
|
"onboarding_reminder_pitch" = "Recibe un recordatorio cada domingo para que la semana siempre empiece planificada.";
|
||||||
|
"onboarding_reminder_allow" = "Recuérdamelo los domingos";
|
||||||
|
"onboarding_reminder_skip" = "Ahora no";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Notificaciones";
|
||||||
|
"settings_weekly_reminder" = "Recordatorio semanal de planificación";
|
||||||
|
"settings_notifications_denied_hint" = "Las notificaciones están desactivadas en los ajustes del sistema.";
|
||||||
|
"settings_open_system_settings" = "Abrir Ajustes";
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "Comment s'est passée cette semaine ?";
|
"week_rating_prompt" = "Comment s'est passée cette semaine ?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "Votre semaine est prête !";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d repas planifiés automatiquement. Vous pourrez tout ajuster ensuite.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Votre planning hebdomadaire est prêt. Ajoutez des plats quand vous voulez et remplissez-le en un geste.";
|
||||||
|
"onboarding_reminder_pitch" = "Recevez un rappel chaque dimanche pour que la semaine commence toujours planifiée.";
|
||||||
|
"onboarding_reminder_allow" = "Rappelez-le-moi le dimanche";
|
||||||
|
"onboarding_reminder_skip" = "Pas maintenant";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Notifications";
|
||||||
|
"settings_weekly_reminder" = "Rappel hebdomadaire de planification";
|
||||||
|
"settings_notifications_denied_hint" = "Les notifications sont désactivées dans les réglages du système.";
|
||||||
|
"settings_open_system_settings" = "Ouvrir Réglages";
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "Com'è andata questa settimana?";
|
"week_rating_prompt" = "Com'è andata questa settimana?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "La tua settimana è pronta!";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d pasti pianificati automaticamente. Potrai modificare tutto in seguito.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Il tuo piano settimanale è pronto. Aggiungi piatti quando vuoi e completalo con un tocco.";
|
||||||
|
"onboarding_reminder_pitch" = "Ricevi un promemoria ogni domenica così la settimana inizia sempre pianificata.";
|
||||||
|
"onboarding_reminder_allow" = "Ricordamelo la domenica";
|
||||||
|
"onboarding_reminder_skip" = "Non ora";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Notifiche";
|
||||||
|
"settings_weekly_reminder" = "Promemoria settimanale di pianificazione";
|
||||||
|
"settings_notifications_denied_hint" = "Le notifiche sono disattivate nelle impostazioni di sistema.";
|
||||||
|
"settings_open_system_settings" = "Apri Impostazioni";
|
||||||
|
|||||||
@@ -362,3 +362,17 @@
|
|||||||
/* Week rating */
|
/* Week rating */
|
||||||
"week_rating_prompt" = "Como foi esta semana?";
|
"week_rating_prompt" = "Como foi esta semana?";
|
||||||
|
|
||||||
|
|
||||||
|
/* Week ready onboarding step (1.2.0) */
|
||||||
|
"onboarding_week_ready_title" = "Sua semana está pronta!";
|
||||||
|
"onboarding_week_ready_subtitle" = "%d refeições planejadas automaticamente. Você pode ajustar tudo depois.";
|
||||||
|
"onboarding_week_ready_subtitle_empty" = "Seu plano semanal está pronto. Adicione pratos quando quiser e preencha com um toque.";
|
||||||
|
"onboarding_reminder_pitch" = "Receba um lembrete todo domingo para que a semana sempre comece planejada.";
|
||||||
|
"onboarding_reminder_allow" = "Lembrar aos domingos";
|
||||||
|
"onboarding_reminder_skip" = "Agora não";
|
||||||
|
|
||||||
|
/* Notification settings (1.2.0) */
|
||||||
|
"settings_notifications_header" = "Notificações";
|
||||||
|
"settings_weekly_reminder" = "Lembrete semanal de planejamento";
|
||||||
|
"settings_notifications_denied_hint" = "As notificações estão desativadas nos ajustes do sistema.";
|
||||||
|
"settings_open_system_settings" = "Abrir Ajustes";
|
||||||
|
|||||||
@@ -4,9 +4,46 @@ import UserNotifications
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class NotificationService {
|
final class NotificationService {
|
||||||
static let shared = NotificationService()
|
static let shared = NotificationService()
|
||||||
|
static let remindersEnabledKey = "notifications_reminders_enabled"
|
||||||
|
|
||||||
|
private static let allReminderIdentifiers = [
|
||||||
|
"mealmood.next-week-planning",
|
||||||
|
"mealmood.sunday-planning"
|
||||||
|
]
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
/// User-level preference for weekly planning reminders (independent of OS permission).
|
||||||
|
var remindersEnabled: Bool {
|
||||||
|
get {
|
||||||
|
UserDefaults.standard.object(forKey: Self.remindersEnabledKey) as? Bool ?? true
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: Self.remindersEnabledKey)
|
||||||
|
if !newValue { cancelAllReminders() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizationStatus() async -> UNAuthorizationStatus {
|
||||||
|
await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicit permission request for contextual prompts (onboarding, settings toggle).
|
||||||
|
/// Returns whether notifications are authorized after the request.
|
||||||
|
@discardableResult
|
||||||
|
func requestPermission() async -> Bool {
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
let settings = await center.notificationSettings()
|
||||||
|
switch settings.authorizationStatus {
|
||||||
|
case .notDetermined:
|
||||||
|
return (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func requestPermissionIfNeeded() async {
|
func requestPermissionIfNeeded() async {
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
let settings = await center.notificationSettings()
|
let settings = await center.notificationSettings()
|
||||||
@@ -14,7 +51,16 @@ final class NotificationService {
|
|||||||
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelAllReminders() {
|
||||||
|
UNUserNotificationCenter.current()
|
||||||
|
.removePendingNotificationRequests(withIdentifiers: Self.allReminderIdentifiers)
|
||||||
|
}
|
||||||
|
|
||||||
func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
||||||
|
guard remindersEnabled else {
|
||||||
|
cancelAllReminders()
|
||||||
|
return
|
||||||
|
}
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
let identifier = "mealmood.next-week-planning"
|
let identifier = "mealmood.next-week-planning"
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ final class OnboardingViewModel: ObservableObject {
|
|||||||
@Published var addedDishes: [(name: String, tagIds: [UUID])] = []
|
@Published var addedDishes: [(name: String, tagIds: [UUID])] = []
|
||||||
let maxOnboardingDishes = 10
|
let maxOnboardingDishes = 10
|
||||||
|
|
||||||
let totalSteps = 6
|
let totalSteps = 7
|
||||||
|
|
||||||
|
/// Slots filled by the in-onboarding auto-fill (aha moment step).
|
||||||
|
@Published var autoFilledCount: Int = 0
|
||||||
|
|
||||||
var canContinue: Bool {
|
var canContinue: Bool {
|
||||||
switch currentStep {
|
switch currentStep {
|
||||||
@@ -37,7 +40,8 @@ final class OnboardingViewModel: ObservableObject {
|
|||||||
case 2: return true // Weekends (toggle)
|
case 2: return true // Weekends (toggle)
|
||||||
case 3: return true // Calendar (optional)
|
case 3: return true // Calendar (optional)
|
||||||
case 4: return true // Dishes are optional
|
case 4: return true // Dishes are optional
|
||||||
case 5: return true // Paywall (always skippable)
|
case 5: return true // Week ready (aha moment + reminder opt-in)
|
||||||
|
case 6: return true // Paywall (always skippable)
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +165,9 @@ final class OnboardingViewModel: ObservableObject {
|
|||||||
AnalyticsService.logDishAdded(tagCount: dishData.tagIds.count, source: "onboarding")
|
AnalyticsService.logDishAdded(tagCount: dishData.tagIds.count, source: "onboarding")
|
||||||
}
|
}
|
||||||
AnalyticsService.logOnboardingCompleted(dishCount: onboardingDishCount)
|
AnalyticsService.logOnboardingCompleted(dishCount: onboardingDishCount)
|
||||||
// If the user added dishes, auto-fill their first week on launch so they
|
// Auto-fill now happens inline in the Week Ready onboarding step, so the
|
||||||
// land on a ready plan instead of an empty one. Otherwise fall back to the
|
// launch flag stays off. Users without dishes still get the home prompt.
|
||||||
// home auto-assign prompt.
|
UserDefaults.standard.set(false, forKey: Self.pendingAutoFillOnLaunchKey)
|
||||||
UserDefaults.standard.set(createdDishes, forKey: Self.pendingAutoFillOnLaunchKey)
|
|
||||||
UserDefaults.standard.set(!createdDishes, forKey: Self.pendingAutoAssignPromptKey)
|
UserDefaults.standard.set(!createdDishes, forKey: Self.pendingAutoAssignPromptKey)
|
||||||
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
||||||
return true
|
return true
|
||||||
@@ -173,4 +176,39 @@ final class OnboardingViewModel: ObservableObject {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fills the first week right inside onboarding (the "aha moment" step) using
|
||||||
|
/// the same engine as the home auto-complete. Returns the number of filled slots.
|
||||||
|
@discardableResult
|
||||||
|
func autoFillFirstWeek(context: ModelContext) -> Int {
|
||||||
|
autoFilledCount = 0
|
||||||
|
guard !addedDishes.isEmpty else { return 0 }
|
||||||
|
|
||||||
|
let dishes = (try? context.fetch(FetchDescriptor<Dish>())) ?? []
|
||||||
|
let tags = (try? context.fetch(FetchDescriptor<Tag>())) ?? []
|
||||||
|
let weekStart = Date().startOfWeek()
|
||||||
|
let plans = (try? context.fetch(FetchDescriptor<WeekPlan>())) ?? []
|
||||||
|
guard let plan = plans.first(where: { $0.weekStartDate == weekStart }) else { return 0 }
|
||||||
|
|
||||||
|
let emptySlots = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }
|
||||||
|
guard !emptySlots.isEmpty else { return 0 }
|
||||||
|
|
||||||
|
let result = AutocompleteEngine.autocomplete(
|
||||||
|
emptySlots: emptySlots,
|
||||||
|
currentPlan: plan,
|
||||||
|
allDishes: dishes,
|
||||||
|
allTags: tags,
|
||||||
|
recentPlans: [],
|
||||||
|
rejectionCounts: FeedbackStore.rejectionCounts()
|
||||||
|
)
|
||||||
|
plan.updatedAt = Date()
|
||||||
|
try? context.save()
|
||||||
|
|
||||||
|
AnalyticsService.logAutoAssignUsed(
|
||||||
|
filledSlots: result.filledSlots.count,
|
||||||
|
unfilledSlots: result.unfilledCount
|
||||||
|
)
|
||||||
|
autoFilledCount = result.filledSlots.count
|
||||||
|
return result.filledSlots.count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,7 +515,8 @@ struct HomeView: View {
|
|||||||
StatsView(weekPlans: weekPlans, allDishes: dishes, allTags: tags, language: settings.languageEnum.resolved())
|
StatsView(weekPlans: weekPlans, allDishes: dishes, allTags: tags, language: settings.languageEnum.resolved())
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await NotificationService.shared.requestPermissionIfNeeded()
|
// Permission is now requested contextually (onboarding Week Ready step or
|
||||||
|
// the Settings toggle) — no cold OS prompt on first Home load.
|
||||||
let nextPlan = fetchWeekPlan(for: Date().startOfWeek().addingDays(7))
|
let nextPlan = fetchWeekPlan(for: Date().startOfWeek().addingDays(7))
|
||||||
NotificationService.shared.schedulePlanningReminderIfNeeded(
|
NotificationService.shared.schedulePlanningReminderIfNeeded(
|
||||||
nextWeekPlan: nextPlan,
|
nextWeekPlan: nextPlan,
|
||||||
@@ -986,6 +987,13 @@ struct HomeView: View {
|
|||||||
let shouldShowPremiumPrompt = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
let shouldShowPremiumPrompt = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||||
guard shouldShowPremiumPrompt else { return }
|
guard shouldShowPremiumPrompt else { return }
|
||||||
|
|
||||||
|
// The paywall was just shown as the final onboarding step — prompting again
|
||||||
|
// 2s later is prompt fatigue. Defer to the second session instead.
|
||||||
|
let sessionCountKey = "post_onboarding_session_count"
|
||||||
|
let sessions = UserDefaults.standard.integer(forKey: sessionCountKey) + 1
|
||||||
|
UserDefaults.standard.set(sessions, forKey: sessionCountKey)
|
||||||
|
guard sessions >= 2 else { return }
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
showPostOnboardingPremiumPrompt = true
|
showPostOnboardingPremiumPrompt = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,18 +64,26 @@ struct OnboardingView: View {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
onFinish: {
|
onFinish: {
|
||||||
_ = viewModel.completeOnboarding(context: context)
|
_ = viewModel.completeOnboarding(context: context)
|
||||||
|
viewModel.autoFillFirstWeek(context: context)
|
||||||
viewModel.nextStep()
|
viewModel.nextStep()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.tag(4)
|
.tag(4)
|
||||||
|
|
||||||
|
WeekReadyStepView(
|
||||||
|
filledCount: viewModel.autoFilledCount,
|
||||||
|
onContinue: { viewModel.nextStep() }
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.tag(5)
|
||||||
|
|
||||||
PaywallStepView(
|
PaywallStepView(
|
||||||
onSkip: { onComplete() },
|
onSkip: { onComplete() },
|
||||||
onConverted: { onComplete() }
|
onConverted: { onComplete() }
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.tag(5)
|
.tag(6)
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
|
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
|
||||||
@@ -126,7 +134,9 @@ struct OnboardingView: View {
|
|||||||
private var onboardingHeader: some View {
|
private var onboardingHeader: some View {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
HStack {
|
HStack {
|
||||||
if viewModel.currentStep > 0 {
|
// No back navigation once onboarding data is committed (step 4 finish):
|
||||||
|
// re-finishing would duplicate dishes and the week plan.
|
||||||
|
if viewModel.currentStep > 0 && viewModel.currentStep < 5 {
|
||||||
Button {
|
Button {
|
||||||
viewModel.previousStep()
|
viewModel.previousStep()
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Onboarding "aha moment" step: shows the freshly auto-filled first week and
|
||||||
|
/// asks for the weekly reminder with context (instead of a cold OS prompt on Home).
|
||||||
|
struct WeekReadyStepView: View {
|
||||||
|
let filledCount: Int
|
||||||
|
var onContinue: () -> Void
|
||||||
|
|
||||||
|
@State private var isRequestingPermission = false
|
||||||
|
@State private var reminderGranted = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.mealMoodCoral.opacity(0.12))
|
||||||
|
.frame(width: 88, height: 88)
|
||||||
|
Image(systemName: filledCount > 0 ? "checkmark.circle.fill" : "calendar.badge.checkmark")
|
||||||
|
.font(.system(size: 44, weight: .semibold))
|
||||||
|
.foregroundColor(.mealMoodCoral)
|
||||||
|
}
|
||||||
|
.padding(.top, 24)
|
||||||
|
|
||||||
|
Text("onboarding_week_ready_title")
|
||||||
|
.font(.mealMoodH1)
|
||||||
|
.foregroundColor(.mealMoodTextPrimary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Text(subtitleText)
|
||||||
|
.font(.mealMoodBody)
|
||||||
|
.foregroundColor(.mealMoodTextSecondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "bell.badge.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundColor(.mealMoodCoral)
|
||||||
|
Text("onboarding_reminder_pitch")
|
||||||
|
.font(.mealMoodSmall)
|
||||||
|
.foregroundColor(.mealMoodTextPrimary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.mealMoodSurface)
|
||||||
|
.cornerRadius(14)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 14)
|
||||||
|
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
PrimaryButton(
|
||||||
|
title: reminderGranted
|
||||||
|
? String(localized: "onboarding_continue")
|
||||||
|
: String(localized: "onboarding_reminder_allow"),
|
||||||
|
action: {
|
||||||
|
if reminderGranted {
|
||||||
|
onContinue()
|
||||||
|
} else {
|
||||||
|
enableReminders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.disabled(isRequestingPermission)
|
||||||
|
|
||||||
|
if !reminderGranted {
|
||||||
|
Button {
|
||||||
|
onContinue()
|
||||||
|
} label: {
|
||||||
|
Text("onboarding_reminder_skip")
|
||||||
|
.font(.mealMoodSmall)
|
||||||
|
.foregroundColor(.mealMoodTextSecondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitleText: String {
|
||||||
|
if filledCount > 0 {
|
||||||
|
return String(format: String(localized: "onboarding_week_ready_subtitle"), filledCount)
|
||||||
|
}
|
||||||
|
return String(localized: "onboarding_week_ready_subtitle_empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func enableReminders() {
|
||||||
|
isRequestingPermission = true
|
||||||
|
Task {
|
||||||
|
let granted = await NotificationService.shared.requestPermission()
|
||||||
|
NotificationService.shared.remindersEnabled = true
|
||||||
|
AnalyticsService.logNotificationsToggled(enabled: granted)
|
||||||
|
isRequestingPermission = false
|
||||||
|
if granted {
|
||||||
|
reminderGranted = true
|
||||||
|
HapticManager.shared.notification(type: .success)
|
||||||
|
// Small beat so the user sees the button flip before moving on.
|
||||||
|
try? await Task.sleep(nanoseconds: 600_000_000)
|
||||||
|
}
|
||||||
|
onContinue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,45 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
import EventKit
|
import EventKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
@Query private var allSettings: [AppSettings]
|
@Query private var allSettings: [AppSettings]
|
||||||
@StateObject private var viewModel = SettingsViewModel()
|
@StateObject private var viewModel = SettingsViewModel()
|
||||||
@State private var showResetAllDataAlert = false
|
@State private var showResetAllDataAlert = false
|
||||||
|
@State private var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
|
||||||
|
@State private var remindersEnabled = NotificationService.shared.remindersEnabled
|
||||||
|
|
||||||
private var settings: AppSettings? { allSettings.first }
|
private var settings: AppSettings? { allSettings.first }
|
||||||
|
|
||||||
|
private func refreshNotificationStatus() async {
|
||||||
|
notificationAuthStatus = await NotificationService.shared.authorizationStatus()
|
||||||
|
remindersEnabled = NotificationService.shared.remindersEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleReminderToggle(_ enabled: Bool, settings: AppSettings) {
|
||||||
|
Task {
|
||||||
|
if enabled {
|
||||||
|
let granted = await NotificationService.shared.requestPermission()
|
||||||
|
NotificationService.shared.remindersEnabled = true
|
||||||
|
remindersEnabled = true
|
||||||
|
notificationAuthStatus = await NotificationService.shared.authorizationStatus()
|
||||||
|
AnalyticsService.logNotificationsToggled(enabled: granted)
|
||||||
|
if granted {
|
||||||
|
NotificationService.shared.schedulePlanningReminderIfNeeded(
|
||||||
|
nextWeekPlan: nil,
|
||||||
|
language: settings.languageEnum.resolved()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NotificationService.shared.remindersEnabled = false
|
||||||
|
remindersEnabled = false
|
||||||
|
AnalyticsService.logNotificationsToggled(enabled: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.mealMoodBackground.ignoresSafeArea()
|
Color.mealMoodBackground.ignoresSafeArea()
|
||||||
@@ -147,6 +177,37 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.listRowBackground(Color.mealMoodSurface)
|
.listRowBackground(Color.mealMoodSurface)
|
||||||
|
|
||||||
|
// Notifications section
|
||||||
|
Section {
|
||||||
|
Toggle("settings_weekly_reminder", isOn: Binding(
|
||||||
|
get: { remindersEnabled && notificationAuthStatus != .denied },
|
||||||
|
set: { newValue in handleReminderToggle(newValue, settings: settings) }
|
||||||
|
))
|
||||||
|
.tint(.mealMoodCoral)
|
||||||
|
.disabled(notificationAuthStatus == .denied)
|
||||||
|
|
||||||
|
if notificationAuthStatus == .denied {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("settings_notifications_denied_hint")
|
||||||
|
.font(.mealMoodSmall)
|
||||||
|
.foregroundColor(.mealMoodTextSecondary)
|
||||||
|
Button {
|
||||||
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("settings_open_system_settings")
|
||||||
|
.font(.mealMoodSmall)
|
||||||
|
.foregroundColor(.mealMoodCoral)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Label("settings_notifications_header", systemImage: "bell")
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.mealMoodSurface)
|
||||||
|
.task { await refreshNotificationStatus() }
|
||||||
|
|
||||||
// Tags section
|
// Tags section
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: TagListView()) {
|
NavigationLink(destination: TagListView()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user