diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index 7ab4e60..2ff73e5 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -8,9 +8,9 @@ /* Begin PBXBuildFile section */ 06E4767B5977FAC8B644FC92 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */; }; - A1B2C3D4E5F607080910ABCD /* DayViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */; }; - 1C0DAB51279DB0FB003B1F21 /* Feels/Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */; }; - 1C0DAB52279DB0FB003B1F22 /* Feels/Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */; }; + 1AB245144C89927264D16645 /* InsightsEmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6988985985DE9C29CFDFA96 /* InsightsEmptyStateTests.swift */; }; + 1C0DAB51279DB0FB003B1F21 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Localizable.xcstrings */; }; + 1C0DAB52279DB0FB003B1F22 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1C0DAB50279DB0FB003B1F21 /* Localizable.xcstrings */; }; 1C9566442EF8F5F70032E68F /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 1C9566432EF8F5F70032E68F /* Algorithms */; }; 1CB4D0A028787D8A00902A56 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB4D09F28787D8A00902A56 /* StoreKit.framework */; }; 1CD90B07278C7DE0001C4FEA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B06278C7DE0001C4FEA /* Tests_iOS.swift */; }; @@ -25,50 +25,52 @@ 1CDE000F2F3BBD26006AE6A1 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 1CA00002300000000000002A /* PostHog */; }; 1CDEFBBF2F3B8736006AE6A1 /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */; }; 1CDEFBC02F3B8736006AE6A1 /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */; }; + 2EE4D94530F6BF39B26FB4D4 /* DayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */; }; 46F07FA9D330456697C9AC29 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD90B47278C7E7A001C4FEA /* WidgetKit.framework */; }; 4F1C717B7747918A459322CB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4D304CD05CC7C662CCD7DCB /* Foundation.framework */; }; 54259F7B3F4E959B3F4055E4 /* StreakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E2A2FC314F88244CA946BF /* StreakTests.swift */; }; - 9559409B5AEEAB40EBCB6AF9 /* VoteLogicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */; }; - EEB21B1CAA8EAEB497BD9FB3 /* DataControllerCRUDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */; }; - A018FE95582C04ED0F1806DC /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */; }; - E0579E66FFBBF124AC625ACD /* WaitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */; }; - C26D40397E1AA24816FB3751 /* TabBarScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */; }; - 2EE4D94530F6BF39B26FB4D4 /* DayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */; }; - A371ED1B0784315F96FFC6BD /* EntryDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */; }; - 92C1523E0398F866DB4CA027 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881CA8B21231D67DED575502 /* SettingsScreen.swift */; }; - AA11110011111100AAAAAAAA /* AppLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11111111111111AAAAAAAA /* AppLaunchTests.swift */; }; - BB22220022222200BBBBBBBB /* MoodLoggingEmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB22222222222222BBBBBBBB /* MoodLoggingEmptyStateTests.swift */; }; - CC33330033333300CCCCCCCC /* MoodLoggingWithDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC33333333333333CCCCCCCC /* MoodLoggingWithDataTests.swift */; }; - DD44440044444400DDDDDDDD /* EntryDetailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD44444444444444DDDDDDDD /* EntryDetailTests.swift */; }; - EE55550055555500EEEEEEEE /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE55555555555555EEEEEEEE /* SettingsTests.swift */; }; - FF66660066666600FFFFFFFF /* SecondaryTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF66666666666666FFFFFFFF /* SecondaryTabTests.swift */; }; - A1B2C3D400000000C9D0E1F2 /* NoteEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */; }; - B2C3D4E500000000D0E1F2A3 /* CustomizeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */; }; - C3D4E500000000E1F2A3B4C5 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */; }; - D4E5F6A700000000F2A3B4C5 /* MoodReplacementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* MoodReplacementTests.swift */; }; - E5F6A7B800000000A3B4C5D6 /* EmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F6A7B8C9D0E1F2A3B4C5D6 /* EmptyStateTests.swift */; }; - F6A7B8C900000000B4C5D6E7 /* EntryDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A7B8C9D0E1F2A3B4C5D6E7 /* EntryDeleteTests.swift */; }; - A7B8C9D000000000C5D6E7F8 /* NotesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8C9D0E1F2A3B4C5D6E7F8 /* NotesTests.swift */; }; - F75470AA2BA1E9EFF8F5265A /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DC4C498A1185DC831F4593 /* LocalizationTests.swift */; }; - E3482DB0421C12E11517BDC8 /* TrialBannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CD463209E0909393545D62 /* TrialBannerTests.swift */; }; - A4B459F8CE7F5534DE4FADCA /* DarkModeStylesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8114D2CE12EC5392371BB415 /* DarkModeStylesTests.swift */; }; - 1AB245144C89927264D16645 /* InsightsEmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6988985985DE9C29CFDFA96 /* InsightsEmptyStateTests.swift */; }; - 756B9857B0657D2DB2D6D4E2 /* AppResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0359E1D32D936859E5A0C9F3 /* AppResumeTests.swift */; }; 6F9C9C4B50CF8C1769171FF9 /* NoteEditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469470483072085BE9E04E12 /* NoteEditTests.swift */; }; - B8C9D0E100000000D6E7F8A9 /* MonthViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9D0E1F2A3B4C5D6E7F8A9 /* MonthViewTests.swift */; }; - C9D0E1F200000000E7F8A9B0 /* SettingsActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0E1F2A3B4C5D6E7F8A9B0 /* SettingsActionTests.swift */; }; - D0E1F2A300000000F8A9B0C1 /* CustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F2A3B4C5D6E7F8A9B0C1 /* CustomizationTests.swift */; }; - E1F2A3B400000000A9B0C1D2 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F2A3B4C5D6E7F8A9B0C1D2 /* OnboardingTests.swift */; }; - F2A3B400000000B0C1D2E3F4 /* StabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A3B4C5D6E7F8A9B0C1D2E3 /* StabilityTests.swift */; }; + 756B9857B0657D2DB2D6D4E2 /* AppResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0359E1D32D936859E5A0C9F3 /* AppResumeTests.swift */; }; + 92C1523E0398F866DB4CA027 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881CA8B21231D67DED575502 /* SettingsScreen.swift */; }; + 9559409B5AEEAB40EBCB6AF9 /* VoteLogicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */; }; + A018FE95582C04ED0F1806DC /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */; }; + A1B2C3D400000000C9D0E1F2 /* NoteEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */; }; + A1B2C3D4E5F607080910ABCD /* DayViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */; }; + A371ED1B0784315F96FFC6BD /* EntryDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */; }; A3B4C5D600000000C1D2E3F4 /* DataPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4C5D6E7F8A9B0C1D2E3F4 /* DataPersistenceTests.swift */; }; - B4C5D6E700000000D2E3F4A5 /* PaywallGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C5D6E7F8A9B0C1D2E3F4A5 /* PaywallGateTests.swift */; }; - C5D6E7F800000000E3F4A5B6 /* AppThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D6E7F8A9B0C1D2E3F4A5B6 /* AppThemeTests.swift */; }; - D6E7F8A900000000F4A5B6C7 /* IconPackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E7F8A9B0C1D2E3F4A5B6C7 /* IconPackTests.swift */; }; - E7F8A9B000000000A5B6C7D8 /* PremiumCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F8A9B0C1D2E3F4A5B6C7D8 /* PremiumCustomizationTests.swift */; }; - F8A9B0C100000000B6C7D8E9 /* HeaderMoodLoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* HeaderMoodLoggingTests.swift */; }; + A4B459F8CE7F5534DE4FADCA /* DarkModeStylesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8114D2CE12EC5392371BB415 /* DarkModeStylesTests.swift */; }; + A7B8C9D000000000C5D6E7F8 /* NotesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8C9D0E1F2A3B4C5D6E7F8 /* NotesTests.swift */; }; A9B0C1D200000000C7D8E9FA /* DayViewGroupingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B0C1D2E3F4A5B6C7D8E9FA /* DayViewGroupingTests.swift */; }; + AA11110011111100AAAAAAAA /* AppLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11111111111111AAAAAAAA /* AppLaunchTests.swift */; }; B0C1D2E300000000D8E9FA0B /* AllDayViewStylesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C1D2E3F4A5B6C7D8E9FA0B /* AllDayViewStylesTests.swift */; }; + B2C3D4E500000000D0E1F2A3 /* CustomizeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */; }; + B4C5D6E700000000D2E3F4A5 /* PaywallGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C5D6E7F8A9B0C1D2E3F4A5 /* PaywallGateTests.swift */; }; + B8C9D0E100000000D6E7F8A9 /* MonthViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9D0E1F2A3B4C5D6E7F8A9 /* MonthViewTests.swift */; }; + BB22220022222200BBBBBBBB /* MoodLoggingEmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB22222222222222BBBBBBBB /* MoodLoggingEmptyStateTests.swift */; }; C1D2E3F400000000E9FA0B1C /* MonthViewInteractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D2E3F4A5B6C7D8E9FA0B1C /* MonthViewInteractionTests.swift */; }; + C26D40397E1AA24816FB3751 /* TabBarScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */; }; + C3D4E500000000E1F2A3B4C5 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */; }; + C5D6E7F800000000E3F4A5B6 /* AppThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D6E7F8A9B0C1D2E3F4A5B6 /* AppThemeTests.swift */; }; + C9D0E1F200000000E7F8A9B0 /* SettingsActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0E1F2A3B4C5D6E7F8A9B0 /* SettingsActionTests.swift */; }; + CC33330033333300CCCCCCCC /* MoodLoggingWithDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC33333333333333CCCCCCCC /* MoodLoggingWithDataTests.swift */; }; + D0E1F2A300000000F8A9B0C1 /* CustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F2A3B4C5D6E7F8A9B0C1 /* CustomizationTests.swift */; }; + D1AD0A0469EADFB1446E9B09 /* YearViewDisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0246E9F406F872E5DEEB7269 /* YearViewDisplayTests.swift */; }; + D4E5F6A700000000F2A3B4C5 /* MoodReplacementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* MoodReplacementTests.swift */; }; + D6E7F8A900000000F4A5B6C7 /* IconPackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E7F8A9B0C1D2E3F4A5B6C7 /* IconPackTests.swift */; }; + DD44440044444400DDDDDDDD /* EntryDetailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD44444444444444DDDDDDDD /* EntryDetailTests.swift */; }; + E0579E66FFBBF124AC625ACD /* WaitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */; }; + E1F2A3B400000000A9B0C1D2 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F2A3B4C5D6E7F8A9B0C1D2 /* OnboardingTests.swift */; }; + E3482DB0421C12E11517BDC8 /* TrialBannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CD463209E0909393545D62 /* TrialBannerTests.swift */; }; + E5F6A7B800000000A3B4C5D6 /* EmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F6A7B8C9D0E1F2A3B4C5D6 /* EmptyStateTests.swift */; }; + E7F8A9B000000000A5B6C7D8 /* PremiumCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F8A9B0C1D2E3F4A5B6C7D8 /* PremiumCustomizationTests.swift */; }; + EE55550055555500EEEEEEEE /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE55555555555555EEEEEEEE /* SettingsTests.swift */; }; + EEB21B1CAA8EAEB497BD9FB3 /* DataControllerCRUDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */; }; + F2A3B400000000B0C1D2E3F4 /* StabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A3B4C5D6E7F8A9B0C1D2E3 /* StabilityTests.swift */; }; + F6A7B8C900000000B4C5D6E7 /* EntryDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A7B8C9D0E1F2A3B4C5D6E7 /* EntryDeleteTests.swift */; }; + F75470AA2BA1E9EFF8F5265A /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DC4C498A1185DC831F4593 /* LocalizationTests.swift */; }; + F8A9B0C100000000B6C7D8E9 /* HeaderMoodLoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* HeaderMoodLoggingTests.swift */; }; + FD30D4508D4C61AB10AC1E71 /* SettingsOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFDAD20AE6C6914EDD87DCBC /* SettingsOnboardingTests.swift */; }; + FF66660066666600FFFFFFFF /* SecondaryTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF66666666666666FFFFFFFF /* SecondaryTabTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -117,7 +119,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Feels/Localizable.xcstrings; sourceTree = ""; }; + 0246E9F406F872E5DEEB7269 /* YearViewDisplayTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YearViewDisplayTests.swift; sourceTree = ""; }; + 0359E1D32D936859E5A0C9F3 /* AppResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppResumeTests.swift; sourceTree = ""; }; + 17DC4C498A1185DC831F4593 /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; + 1C0DAB50279DB0FB003B1F21 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Feels/Localizable.xcstrings; sourceTree = ""; }; 1CB4D09F28787D8A00902A56 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.5.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; 1CD90AF5278C7DE0001C4FEA /* Feels.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Feels.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1CD90AFB278C7DE0001C4FEA /* Feels.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Feels.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -139,58 +144,57 @@ 1CD90B70278C8000001C4FEA /* Feels (iOS)Dev.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Feels (iOS)Dev.entitlements"; sourceTree = ""; }; 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; 1E594AEAB5F046E3B3ED7C47 /* Feels Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Feels Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 29E2A2FC314F88244CA946BF /* StreakTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StreakTests.swift; sourceTree = ""; }; - 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataControllerCRUDTests.swift; sourceTree = ""; }; - 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; - D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DayViewViewModelTests.swift; sourceTree = ""; }; - B60015D02A064FF582E232FD /* Feels Watch App/Feels Watch AppDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch AppDebug.entitlements"; sourceTree = ""; }; - B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App/Feels Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch App.entitlements"; sourceTree = ""; }; - DA0D74ACDD741CFA1F14F50F /* FeelsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeelsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoteLogicsTests.swift; sourceTree = ""; }; - F4D304CD05CC7C662CCD7DCB /* 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; }; - 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; - 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitHelpers.swift; sourceTree = ""; }; - C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScreen.swift; sourceTree = ""; }; - 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayScreen.swift; sourceTree = ""; }; - 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailScreen.swift; sourceTree = ""; }; - 881CA8B21231D67DED575502 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; - AA11111111111111AAAAAAAA /* AppLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLaunchTests.swift; sourceTree = ""; }; - BB22222222222222BBBBBBBB /* MoodLoggingEmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodLoggingEmptyStateTests.swift; sourceTree = ""; }; - CC33333333333333CCCCCCCC /* MoodLoggingWithDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodLoggingWithDataTests.swift; sourceTree = ""; }; - DD44444444444444DDDDDDDD /* EntryDetailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailTests.swift; sourceTree = ""; }; - EE55555555555555EEEEEEEE /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; - FF66666666666666FFFFFFFF /* SecondaryTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTabTests.swift; sourceTree = ""; }; - A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorScreen.swift; sourceTree = ""; }; - B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeScreen.swift; sourceTree = ""; }; - C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; - D4E5F6A7B8C9D0E1F2A3B4C5 /* MoodReplacementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodReplacementTests.swift; sourceTree = ""; }; - E5F6A7B8C9D0E1F2A3B4C5D6 /* EmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateTests.swift; sourceTree = ""; }; - F6A7B8C9D0E1F2A3B4C5D6E7 /* EntryDeleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDeleteTests.swift; sourceTree = ""; }; - A7B8C9D0E1F2A3B4C5D6E7F8 /* NotesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTests.swift; sourceTree = ""; }; - 17DC4C498A1185DC831F4593 /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; 21CD463209E0909393545D62 /* TrialBannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialBannerTests.swift; sourceTree = ""; }; - 8114D2CE12EC5392371BB415 /* DarkModeStylesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeStylesTests.swift; sourceTree = ""; }; - A6988985985DE9C29CFDFA96 /* InsightsEmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsEmptyStateTests.swift; sourceTree = ""; }; - 0359E1D32D936859E5A0C9F3 /* AppResumeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppResumeTests.swift; sourceTree = ""; }; + 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; + 29E2A2FC314F88244CA946BF /* StreakTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StreakTests.swift; sourceTree = ""; }; + 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayScreen.swift; sourceTree = ""; }; 469470483072085BE9E04E12 /* NoteEditTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditTests.swift; sourceTree = ""; }; - B8C9D0E1F2A3B4C5D6E7F8A9 /* MonthViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthViewTests.swift; sourceTree = ""; }; - C9D0E1F2A3B4C5D6E7F8A9B0 /* SettingsActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsActionTests.swift; sourceTree = ""; }; - D0E1F2A3B4C5D6E7F8A9B0C1 /* CustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizationTests.swift; sourceTree = ""; }; - E1F2A3B4C5D6E7F8A9B0C1D2 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; - F2A3B4C5D6E7F8A9B0C1D2E3 /* StabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StabilityTests.swift; sourceTree = ""; }; + 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitHelpers.swift; sourceTree = ""; }; + 5566271983AEDF1D33C34FE6 /* DataControllerCRUDTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataControllerCRUDTests.swift; sourceTree = ""; }; + 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailScreen.swift; sourceTree = ""; }; + 8114D2CE12EC5392371BB415 /* DarkModeStylesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeStylesTests.swift; sourceTree = ""; }; + 881CA8B21231D67DED575502 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; + 9CFAE86F485C853DB3239DD9 /* IntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; + A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorScreen.swift; sourceTree = ""; }; A3B4C5D6E7F8A9B0C1D2E3F4 /* DataPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPersistenceTests.swift; sourceTree = ""; }; - B4C5D6E7F8A9B0C1D2E3F4A5 /* PaywallGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallGateTests.swift; sourceTree = ""; }; - C5D6E7F8A9B0C1D2E3F4A5B6 /* AppThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppThemeTests.swift; sourceTree = ""; }; - D6E7F8A9B0C1D2E3F4A5B6C7 /* IconPackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconPackTests.swift; sourceTree = ""; }; - E7F8A9B0C1D2E3F4A5B6C7D8 /* PremiumCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumCustomizationTests.swift; sourceTree = ""; }; - F8A9B0C1D2E3F4A5B6C7D8E9 /* HeaderMoodLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderMoodLoggingTests.swift; sourceTree = ""; }; + A6988985985DE9C29CFDFA96 /* InsightsEmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsEmptyStateTests.swift; sourceTree = ""; }; + A7B8C9D0E1F2A3B4C5D6E7F8 /* NotesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTests.swift; sourceTree = ""; }; A9B0C1D2E3F4A5B6C7D8E9FA /* DayViewGroupingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayViewGroupingTests.swift; sourceTree = ""; }; + AA11111111111111AAAAAAAA /* AppLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLaunchTests.swift; sourceTree = ""; }; B0C1D2E3F4A5B6C7D8E9FA0B /* AllDayViewStylesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDayViewStylesTests.swift; sourceTree = ""; }; + B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeScreen.swift; sourceTree = ""; }; + B4C5D6E7F8A9B0C1D2E3F4A5 /* PaywallGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallGateTests.swift; sourceTree = ""; }; + B60015D02A064FF582E232FD /* Feels Watch AppDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch AppDebug.entitlements"; sourceTree = ""; }; + B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Feels Watch App/Feels Watch App.entitlements"; sourceTree = ""; }; + B8C9D0E1F2A3B4C5D6E7F8A9 /* MonthViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthViewTests.swift; sourceTree = ""; }; + BB22222222222222BBBBBBBB /* MoodLoggingEmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodLoggingEmptyStateTests.swift; sourceTree = ""; }; C1D2E3F4A5B6C7D8E9FA0B1C /* MonthViewInteractionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthViewInteractionTests.swift; sourceTree = ""; }; + C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; + C5D6E7F8A9B0C1D2E3F4A5B6 /* AppThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppThemeTests.swift; sourceTree = ""; }; + C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScreen.swift; sourceTree = ""; }; + C9D0E1F2A3B4C5D6E7F8A9B0 /* SettingsActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsActionTests.swift; sourceTree = ""; }; + CC33333333333333CCCCCCCC /* MoodLoggingWithDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodLoggingWithDataTests.swift; sourceTree = ""; }; + D0E1F2A3B4C5D6E7F8A9B0C1 /* CustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizationTests.swift; sourceTree = ""; }; + D4E5F60708091011ABCDE001 /* DayViewViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DayViewViewModelTests.swift; sourceTree = ""; }; + D4E5F6A7B8C9D0E1F2A3B4C5 /* MoodReplacementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodReplacementTests.swift; sourceTree = ""; }; + D6E7F8A9B0C1D2E3F4A5B6C7 /* IconPackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconPackTests.swift; sourceTree = ""; }; + DA0D74ACDD741CFA1F14F50F /* FeelsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeelsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DD44444444444444DDDDDDDD /* EntryDetailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailTests.swift; sourceTree = ""; }; + DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoteLogicsTests.swift; sourceTree = ""; }; + DFDAD20AE6C6914EDD87DCBC /* SettingsOnboardingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsOnboardingTests.swift; sourceTree = ""; }; + E1F2A3B4C5D6E7F8A9B0C1D2 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; + E5F6A7B8C9D0E1F2A3B4C5D6 /* EmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateTests.swift; sourceTree = ""; }; + E7F8A9B0C1D2E3F4A5B6C7D8 /* PremiumCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumCustomizationTests.swift; sourceTree = ""; }; + EE55555555555555EEEEEEEE /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; + F2A3B4C5D6E7F8A9B0C1D2E3 /* StabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StabilityTests.swift; sourceTree = ""; }; + F4D304CD05CC7C662CCD7DCB /* 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; }; + F6A7B8C9D0E1F2A3B4C5D6E7 /* EntryDeleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDeleteTests.swift; sourceTree = ""; }; + F8A9B0C1D2E3F4A5B6C7D8E9 /* HeaderMoodLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderMoodLoggingTests.swift; sourceTree = ""; }; + FF66666666666666FFFFFFFF /* SecondaryTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTabTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 1C000C162EE93AE3009C9ED5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 1C000C162EE93AE3009C9ED5 /* Exceptions for "Shared" folder in "FeelsWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( AccessibilityIdentifiers.swift, @@ -221,7 +225,7 @@ ); target = 1CD90B44278C7E7A001C4FEA /* FeelsWidgetExtension */; }; - 2166CE8AA7264FC2B4BFAAAC /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 2166CE8AA7264FC2B4BFAAAC /* Exceptions for "Shared" folder in "Feels Watch App" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Models/Mood.swift, @@ -236,9 +240,41 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 1C00073D2EE9388A009C9ED5 /* Shared */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (2166CE8AA7264FC2B4BFAAAC /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 1C000C162EE93AE3009C9ED5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Shared; sourceTree = ""; }; - 1C0009922EE938FC009C9ED5 /* FeelsWidget2 */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FeelsWidget2; sourceTree = ""; }; - 579031D619ED4B989145EEB1 /* Feels Watch App */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Feels Watch App"; sourceTree = ""; }; + 1C00073D2EE9388A009C9ED5 /* Shared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2166CE8AA7264FC2B4BFAAAC /* Exceptions for "Shared" folder in "Feels Watch App" target */, + 1C000C162EE93AE3009C9ED5 /* Exceptions for "Shared" folder in "FeelsWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Shared; + sourceTree = ""; + }; + 1C0009922EE938FC009C9ED5 /* FeelsWidget2 */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = FeelsWidget2; + sourceTree = ""; + }; + 579031D619ED4B989145EEB1 /* Feels Watch App */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = "Feels Watch App"; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -306,9 +342,9 @@ 1CD90AE5278C7DDF001C4FEA = { isa = PBXGroup; children = ( - B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App/Feels Watch App.entitlements */, - B60015D02A064FF582E232FD /* Feels Watch App/Feels Watch AppDebug.entitlements */, - 1C0DAB50279DB0FB003B1F21 /* Feels/Localizable.xcstrings */, + B8AB4CD73C2B4DC89C6FE84D /* Feels Watch App.entitlements */, + B60015D02A064FF582E232FD /* Feels Watch AppDebug.entitlements */, + 1C0DAB50279DB0FB003B1F21 /* Localizable.xcstrings */, 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */, 1CD90B6A278C7F75001C4FEA /* Feels (iOS).entitlements */, 1CD90B70278C8000001C4FEA /* Feels (iOS)Dev.entitlements */, @@ -386,33 +422,12 @@ A9B0C1D2E3F4A5B6C7D8E9FA /* DayViewGroupingTests.swift */, B0C1D2E3F4A5B6C7D8E9FA0B /* AllDayViewStylesTests.swift */, C1D2E3F4A5B6C7D8E9FA0B1C /* MonthViewInteractionTests.swift */, + 0246E9F406F872E5DEEB7269 /* YearViewDisplayTests.swift */, + DFDAD20AE6C6914EDD87DCBC /* SettingsOnboardingTests.swift */, ); path = "Tests iOS"; sourceTree = ""; }; - 3A62ED77167DA212DE1CCB7D /* Helpers */ = { - isa = PBXGroup; - children = ( - 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */, - 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - B697A2092711045D69109EA1 /* Screens */ = { - isa = PBXGroup; - children = ( - C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */, - 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */, - 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */, - 881CA8B21231D67DED575502 /* SettingsScreen.swift */, - A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */, - B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */, - C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */, - ); - path = Screens; - sourceTree = ""; - }; 1CD90B11278C7DE0001C4FEA /* Tests macOS */ = { isa = PBXGroup; children = ( @@ -446,6 +461,15 @@ path = FeelsTests; sourceTree = ""; }; + 3A62ED77167DA212DE1CCB7D /* Helpers */ = { + isa = PBXGroup; + children = ( + 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */, + 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 88F4C25CA0D11FB136B0B8A6 /* iOS */ = { isa = PBXGroup; children = ( @@ -454,6 +478,20 @@ name = iOS; sourceTree = ""; }; + B697A2092711045D69109EA1 /* Screens */ = { + isa = PBXGroup; + children = ( + C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */, + 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */, + 7E35564DEA72EB6F8447CDAA /* EntryDetailScreen.swift */, + 881CA8B21231D67DED575502 /* SettingsScreen.swift */, + A1B2C3D4E5F6A7B8C9D0E1F2 /* NoteEditorScreen.swift */, + B2C3D4E5F6A7B8C9D0E1F2A3 /* CustomizeScreen.swift */, + C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -676,7 +714,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1C0DAB51279DB0FB003B1F21 /* Feels/Localizable.xcstrings in Resources */, + 1C0DAB51279DB0FB003B1F21 /* Localizable.xcstrings in Resources */, 1CDEFBBF2F3B8736006AE6A1 /* Configuration.storekit in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -707,7 +745,7 @@ buildActionMask = 2147483647; files = ( 1CDEFBC02F3B8736006AE6A1 /* Configuration.storekit in Resources */, - 1C0DAB52279DB0FB003B1F22 /* Feels/Localizable.xcstrings in Resources */, + 1C0DAB52279DB0FB003B1F22 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -787,6 +825,8 @@ A9B0C1D200000000C7D8E9FA /* DayViewGroupingTests.swift in Sources */, B0C1D2E300000000D8E9FA0B /* AllDayViewStylesTests.swift in Sources */, C1D2E3F400000000E9FA0B1C /* MonthViewInteractionTests.swift in Sources */, + D1AD0A0469EADFB1446E9B09 /* YearViewDisplayTests.swift in Sources */, + FD30D4508D4C61AB10AC1E71 /* SettingsOnboardingTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Shared/AccessibilityIdentifiers.swift b/Shared/AccessibilityIdentifiers.swift index 5591b99..62084ef 100644 --- a/Shared/AccessibilityIdentifiers.swift +++ b/Shared/AccessibilityIdentifiers.swift @@ -125,6 +125,11 @@ enum AccessibilityID { // MARK: - Year View enum YearView { static let heatmap = "year_heatmap" + static let donutChart = "year_donut_chart" + static let barChart = "year_bar_chart" + static let statsSection = "year_stats_section" + static func cardHeader(year: Int) -> String { "year_card_header_\(year)" } + static let shareButton = "year_share_button" } // MARK: - Onboarding diff --git a/Shared/Onboarding/views/OnboardingDay.swift b/Shared/Onboarding/views/OnboardingDay.swift index 29a5302..813424e 100644 --- a/Shared/Onboarding/views/OnboardingDay.swift +++ b/Shared/Onboarding/views/OnboardingDay.swift @@ -25,16 +25,7 @@ struct OnboardingDay: View { @ObservedObject var onboardingData: OnboardingData var body: some View { - ZStack { - // Gradient background - LinearGradient( - colors: [Color(hex: "4facfe"), Color(hex: "00f2fe")], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() - - VStack(spacing: 0) { + VStack(spacing: 0) { Spacer() // Icon @@ -103,8 +94,15 @@ struct OnboardingDay: View { } .padding(.horizontal, 30) .padding(.bottom, 80) - } } + .background( + LinearGradient( + colors: [Color(hex: "4facfe"), Color(hex: "00f2fe")], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + ) .accessibilityIdentifier(AccessibilityID.Onboarding.dayScreen) } } diff --git a/Shared/Onboarding/views/OnboardingStyle.swift b/Shared/Onboarding/views/OnboardingStyle.swift index fc84608..f5f376d 100644 --- a/Shared/Onboarding/views/OnboardingStyle.swift +++ b/Shared/Onboarding/views/OnboardingStyle.swift @@ -12,17 +12,7 @@ struct OnboardingStyle: View { @State private var selectedTheme: AppTheme = .celestial var body: some View { - ZStack { - // Gradient background that updates with theme - LinearGradient( - colors: selectedTheme.previewColors, - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() - .animation(.easeInOut(duration: 0.4), value: selectedTheme) - - ScrollView(showsIndicators: false) { + ScrollView(showsIndicators: false) { VStack(spacing: 0) { // Icon ZStack { @@ -85,8 +75,16 @@ struct OnboardingStyle: View { .padding(.top, 24) .padding(.bottom, 80) } - } } + .background( + LinearGradient( + colors: selectedTheme.previewColors, + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + .animation(.easeInOut(duration: 0.4), value: selectedTheme) + ) .onAppear { // Apply default theme on appear selectedTheme.apply() diff --git a/Shared/Onboarding/views/OnboardingSubscription.swift b/Shared/Onboarding/views/OnboardingSubscription.swift index a23b254..6360cb0 100644 --- a/Shared/Onboarding/views/OnboardingSubscription.swift +++ b/Shared/Onboarding/views/OnboardingSubscription.swift @@ -15,16 +15,7 @@ struct OnboardingSubscription: View { let completionClosure: ((OnboardingData) -> Void) var body: some View { - ZStack { - // Gradient background - LinearGradient( - colors: [Color(hex: "11998e"), Color(hex: "38ef7d")], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .ignoresSafeArea() - - VStack(spacing: 0) { + VStack(spacing: 0) { Spacer() // Crown icon @@ -137,8 +128,15 @@ struct OnboardingSubscription: View { } .padding(.horizontal, 24) .padding(.bottom, 50) - } } + .background( + LinearGradient( + colors: [Color(hex: "11998e"), Color(hex: "38ef7d")], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + ) .accessibilityIdentifier(AccessibilityID.Onboarding.subscriptionScreen) .sheet(isPresented: $showSubscriptionStore, onDismiss: { // After subscription store closes, complete onboarding diff --git a/Shared/Onboarding/views/OnboardingTitle.swift b/Shared/Onboarding/views/OnboardingTitle.swift index 3680dce..b86041d 100644 --- a/Shared/Onboarding/views/OnboardingTitle.swift +++ b/Shared/Onboarding/views/OnboardingTitle.swift @@ -16,44 +16,35 @@ struct OnboardingTitle: View { @ObservedObject var onboardingData: OnboardingData var body: some View { - ZStack { - - Image("average", bundle: .main) - .foregroundColor(Color(UIColor.darkText)) - .opacity(0.04) - .scaleEffect(1.2) - .padding(.bottom, 55) - .accessibilityHidden(true) - - ScrollView { - VStack{ - Text(String(localized: "onboarding_title_title")) - .font(.title) - .foregroundColor(Color(UIColor.white)) - .padding([.trailing, .leading], 55) - .padding([.top], 25) - - ForEach(OnboardingTitle.titleOptions, id: \.self) { option in - Button(action: { + ScrollView { + VStack{ + Text(String(localized: "onboarding_title_title")) + .font(.title) + .foregroundColor(Color(UIColor.white)) + .padding([.trailing, .leading], 55) + .padding([.top], 25) + + ForEach(OnboardingTitle.titleOptions, id: \.self) { option in + Button(action: { // onboardingData.title = option - }, label: { - Text(option) - .font(.subheadline.weight(.bold)) - .foregroundColor(.white) - .padding(10) - .background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white)) - .cornerRadius(10) - }) - .buttonStyle(PlainButtonStyle()) - .padding([.top], 10) - } - - Text(String(localized: "onboarding_title_type_your_own")) - .font(.body) - .foregroundColor(Color(UIColor.white)) - .padding([.top], 25) - .padding([.trailing, .leading], 55) - + }, label: { + Text(option) + .font(.subheadline.weight(.bold)) + .foregroundColor(.white) + .padding(10) + .background(RoundedRectangle(cornerRadius: 10).stroke().foregroundColor(Color.white)) + .cornerRadius(10) + }) + .buttonStyle(PlainButtonStyle()) + .padding([.top], 10) + } + + Text(String(localized: "onboarding_title_type_your_own")) + .font(.body) + .foregroundColor(Color(UIColor.white)) + .padding([.top], 25) + .padding([.trailing, .leading], 55) + // TextField("Notification", text: $onboardingData.title) // .frame(height: 44) // .foregroundColor(Color(UIColor.white)) @@ -63,12 +54,21 @@ struct OnboardingTitle: View { // .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.white)) // .padding([.leading, .trailing], 75) // .padding([.top], 45) - - Spacer() - } + + Spacer() + } + } + .background { + ZStack { + Color.orange + Image("average", bundle: .main) + .foregroundColor(Color(UIColor.darkText)) + .opacity(0.04) + .scaleEffect(1.2) + .padding(.bottom, 55) + .accessibilityHidden(true) } } - .background(.orange) } } diff --git a/Shared/Onboarding/views/OnboardingWrapup.swift b/Shared/Onboarding/views/OnboardingWrapup.swift index 4f087ce..93f1e10 100644 --- a/Shared/Onboarding/views/OnboardingWrapup.swift +++ b/Shared/Onboarding/views/OnboardingWrapup.swift @@ -19,8 +19,56 @@ struct OnboardingWrapup: View { } var body: some View { - ZStack { - GeometryReader { geometry in + GeometryReader { geometry in + VStack { + ScrollView { + + Spacer() + + Text(String(localized: "onboarding_wrap_up_1")) + .padding(.top) + .padding() + .font(.title) + .foregroundColor(Color(UIColor.white)) + + Text(formatter.string(from: onboardingData.date)) + .font(.title) + .fontWeight(.bold) + .padding() + .foregroundColor(Color(UIColor.white)) + + Text(String(localized: "onboarding_wrap_up_3")) + .font(.title) + .padding() + .foregroundColor(Color(UIColor.white)) + + Text(onboardingData.inputDay.localizedValue) + .font(.title) + .fontWeight(.bold) + .padding() + .foregroundColor(Color(UIColor.white)) + + Button(action: { + AnalyticsManager.shared.track(.onboardingCompleted(dayId: String(onboardingData.inputDay.rawValue))) + completionClosure(onboardingData) + }, label: { + Text(String(localized: "onboarding_wrap_up_complete_button")) + .font(.title) + .fontWeight(.bold) + .foregroundColor(Color(hex: "31d158")) + .padding() + .background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(Color.white)) + .cornerRadius(10) + }) + .padding([.top], 65) + } + .multilineTextAlignment(.center) + } + .frame(maxWidth: geometry.size.width) + } + .background { + ZStack { + Color(hex: "31d158") VStack { Spacer() Image("great", bundle: .main) @@ -30,55 +78,8 @@ struct OnboardingWrapup: View { .accessibilityHidden(true) Spacer() } - - VStack { - ScrollView { - - Spacer() - - Text(String(localized: "onboarding_wrap_up_1")) - .padding(.top) - .padding() - .font(.title) - .foregroundColor(Color(UIColor.white)) - - Text(formatter.string(from: onboardingData.date)) - .font(.title) - .fontWeight(.bold) - .padding() - .foregroundColor(Color(UIColor.white)) - - Text(String(localized: "onboarding_wrap_up_3")) - .font(.title) - .padding() - .foregroundColor(Color(UIColor.white)) - - Text(onboardingData.inputDay.localizedValue) - .font(.title) - .fontWeight(.bold) - .padding() - .foregroundColor(Color(UIColor.white)) - - Button(action: { - AnalyticsManager.shared.track(.onboardingCompleted(dayId: String(onboardingData.inputDay.rawValue))) - completionClosure(onboardingData) - }, label: { - Text(String(localized: "onboarding_wrap_up_complete_button")) - .font(.title) - .fontWeight(.bold) - .foregroundColor(Color(hex: "31d158")) - .padding() - .background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(Color.white)) - .cornerRadius(10) - }) - .padding([.top], 65) - } - .multilineTextAlignment(.center) - } - .frame(maxWidth: geometry.size.width) } } - .background(Color(hex: "31d158")) } } diff --git a/Shared/Views/AddMoodHeaderView.swift b/Shared/Views/AddMoodHeaderView.swift index 8b44ff4..1b275a1 100644 --- a/Shared/Views/AddMoodHeaderView.swift +++ b/Shared/Views/AddMoodHeaderView.swift @@ -41,8 +41,6 @@ struct AddMoodHeaderView: View { Text(String(imagePack.rawValue)) .hidden() - theme.currentTheme.secondaryBGColor - VStack(spacing: 16) { Text(ShowBasedOnVoteLogics.getVotingTitle(onboardingData: onboardingData)) .font(.title2.bold()) @@ -66,6 +64,7 @@ struct AddMoodHeaderView: View { } } } + .background(theme.currentTheme.secondaryBGColor) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .fixedSize(horizontal: false, vertical: true) .accessibilityIdentifier(AccessibilityID.DayView.moodHeader) diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 6e9a6f5..1fc5e8a 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -24,17 +24,17 @@ struct CustomizeContentView: View { Button(action: { showThemePicker = true }) { HStack(spacing: 16) { // Emoji preview - ZStack { - LinearGradient( - colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)], - startPoint: .topLeading, - endPoint: .bottomTrailing + Text("๐ŸŽจ") + .font(.title) + .frame(width: 56, height: 56) + .background( + LinearGradient( + colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) ) - Text("๐ŸŽจ") - .font(.title) - } - .frame(width: 56, height: 56) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading, spacing: 4) { Text("Browse Themes") diff --git a/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift b/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift index b7d7c79..807c38d 100644 --- a/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift @@ -259,26 +259,25 @@ struct AppThemePreviewSheet: View { } private var heroSection: some View { - ZStack { - // Gradient background + VStack(spacing: 16) { + Text(theme.emoji) + .font(.system(size: 72)) + .shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4) + + Text(theme.tagline) + .font(.title3.weight(.medium)) + .foregroundColor(.white) + .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) + } + .frame(maxWidth: .infinity) + .frame(height: 200) + .background( LinearGradient( colors: theme.previewColors + [theme.previewColors[0].opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing ) - - VStack(spacing: 16) { - Text(theme.emoji) - .font(.system(size: 72)) - .shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4) - - Text(theme.tagline) - .font(.title3.weight(.medium)) - .foregroundColor(.white) - .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) - } - } - .frame(height: 200) + ) .clipShape(RoundedRectangle(cornerRadius: 20)) .padding(.horizontal, 20) .padding(.top, 16) diff --git a/Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift b/Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift index bf72c38..bb7807c 100644 --- a/Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift @@ -22,40 +22,37 @@ struct DayFilterPickerView: View { (Calendar.current.shortWeekdaySymbols[6], 7)] var body: some View { - ZStack { - theme.currentTheme.secondaryBGColor - - VStack { - HStack { - ForEach(weekdays.indices, id: \.self) { dayIdx in - let day = String(weekdays[dayIdx].0) - let value = weekdays[dayIdx].1 - let isSelected = filteredDays.currentFilters.contains(value) - Button(action: { - if isSelected { - filteredDays.removeFilter(filter: value) - } else { - filteredDays.addFilter(newFilter: value) - } - let impactMed = UIImpactFeedbackGenerator(style: .heavy) - impactMed.impactOccurred() - }) { - Text(day.capitalized) - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color(uiColor: .tertiarySystemBackground)) - .foregroundColor(isSelected ? .green : .red) - .cornerRadius(8) + VStack { + HStack { + ForEach(weekdays.indices, id: \.self) { dayIdx in + let day = String(weekdays[dayIdx].0) + let value = weekdays[dayIdx].1 + let isSelected = filteredDays.currentFilters.contains(value) + Button(action: { + if isSelected { + filteredDays.removeFilter(filter: value) + } else { + filteredDays.addFilter(newFilter: value) } - .buttonStyle(.plain) + let impactMed = UIImpactFeedbackGenerator(style: .heavy) + impactMed.impactOccurred() + }) { + Text(day.capitalized) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color(uiColor: .tertiarySystemBackground)) + .foregroundColor(isSelected ? .green : .red) + .cornerRadius(8) } + .buttonStyle(.plain) } - Text(String(localized: "day_picker_view_text")) - .padding(.top) - .foregroundColor(textColor) } - .padding() + Text(String(localized: "day_picker_view_text")) + .padding(.top) + .foregroundColor(textColor) } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } diff --git a/Shared/Views/CustomizeView/SubViews/IconPickerView.swift b/Shared/Views/CustomizeView/SubViews/IconPickerView.swift index ca6b9ff..e0af28a 100644 --- a/Shared/Views/CustomizeView/SubViews/IconPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/IconPickerView.swift @@ -50,47 +50,45 @@ struct IconPickerView: View { ] var body: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - ScrollView(.horizontal) { - HStack { + VStack { + ScrollView(.horizontal) { + HStack { + Button(action: { + UIApplication.shared.setAlternateIconName(nil) + AnalyticsManager.shared.track(.appIconChanged(iconTitle: "default")) + }, label: { + Image("AppIconImage", bundle: .main) + .resizable() + .frame(width: 50, height:50) + .cornerRadius(10) + }) + .accessibilityLabel(String(localized: "Default app icon")) + .accessibilityHint(String(localized: "Double tap to select")) + + + ForEach(iconSets, id: \.self.0){ iconSet in Button(action: { - UIApplication.shared.setAlternateIconName(nil) - AnalyticsManager.shared.track(.appIconChanged(iconTitle: "default")) + UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in + // FIXME: Handle error + } + AnalyticsManager.shared.track(.appIconChanged(iconTitle: iconSet.1)) }, label: { - Image("AppIconImage", bundle: .main) + Image(iconSet.0, bundle: .main) .resizable() .frame(width: 50, height:50) .cornerRadius(10) }) - .accessibilityLabel(String(localized: "Default app icon")) + .accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))")) .accessibilityHint(String(localized: "Double tap to select")) - - - ForEach(iconSets, id: \.self.0){ iconSet in - Button(action: { - UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in - // FIXME: Handle error - } - AnalyticsManager.shared.track(.appIconChanged(iconTitle: iconSet.1)) - }, label: { - Image(iconSet.0, bundle: .main) - .resizable() - .frame(width: 50, height:50) - .cornerRadius(10) - }) - .accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))")) - .accessibilityHint(String(localized: "Double tap to select")) - } } - .padding() } - .background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor)) .padding() - .cornerRadius(10) } + .background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor)) + .padding() + .cornerRadius(10) } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } diff --git a/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift b/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift index 9615508..0670992 100644 --- a/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift @@ -16,58 +16,56 @@ struct PersonalityPackPickerView: View { private var textColor: Color { theme.currentTheme.labelColor } var body: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - ForEach(PersonalityPack.allCases, id: \.self) { aPack in - VStack(spacing: 10) { - Text(String(aPack.title())) - .font(.body) - .foregroundColor(textColor) - - Text(aPack.randomPushNotificationStrings().title) - .font(.body) - .foregroundColor(Color(UIColor.systemGray)) - Text(aPack.randomPushNotificationStrings().body) - .font(.body) - .foregroundColor(Color(UIColor.systemGray)) - } - .frame(minWidth: 0, maxWidth: .infinity) - .padding() - .contentShape(Rectangle()) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(personalityPack == aPack ? theme.currentTheme.bgColor : .clear) - .padding(5) - ) - .onTapGesture { - let impactMed = UIImpactFeedbackGenerator(style: .heavy) - impactMed.impactOccurred() - personalityPack = aPack - AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title())) - LocalNotification.rescheduleNotifiations() -// } - } -// .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0) - .alert(isPresented: $showOver18Alert) { - let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) { - showNSFW = true - } - let secondaryButton = Alert.Button.cancel(Text(String(localized: "customize_view_over18alert_no"))) { - showNSFW = false - } - return Alert(title: Text(String(localized: "customize_view_over18alert_title")), - message: Text(String(localized: "customize_view_over18alert_body")), - primaryButton: primaryButton, - secondaryButton: secondaryButton) - } - if aPack.rawValue != (PersonalityPack.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 { - Divider() - } + VStack { + ForEach(PersonalityPack.allCases, id: \.self) { aPack in + VStack(spacing: 10) { + Text(String(aPack.title())) + .font(.body) + .foregroundColor(textColor) + + Text(aPack.randomPushNotificationStrings().title) + .font(.body) + .foregroundColor(Color(UIColor.systemGray)) + Text(aPack.randomPushNotificationStrings().body) + .font(.body) + .foregroundColor(Color(UIColor.systemGray)) + } + .frame(minWidth: 0, maxWidth: .infinity) + .padding() + .contentShape(Rectangle()) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(personalityPack == aPack ? theme.currentTheme.bgColor : .clear) + .padding(5) + ) + .onTapGesture { + let impactMed = UIImpactFeedbackGenerator(style: .heavy) + impactMed.impactOccurred() + personalityPack = aPack + AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title())) + LocalNotification.rescheduleNotifiations() +// } + } +// .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0) + .alert(isPresented: $showOver18Alert) { + let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) { + showNSFW = true + } + let secondaryButton = Alert.Button.cancel(Text(String(localized: "customize_view_over18alert_no"))) { + showNSFW = false + } + return Alert(title: Text(String(localized: "customize_view_over18alert_title")), + message: Text(String(localized: "customize_view_over18alert_body")), + primaryButton: primaryButton, + secondaryButton: secondaryButton) + } + if aPack.rawValue != (PersonalityPack.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 { + Divider() } - } + } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } diff --git a/Shared/Views/CustomizeView/SubViews/ThemePickerView.swift b/Shared/Views/CustomizeView/SubViews/ThemePickerView.swift index 20507fb..8efc30b 100644 --- a/Shared/Views/CustomizeView/SubViews/ThemePickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/ThemePickerView.swift @@ -15,19 +15,17 @@ struct ThemePickerView: View { private var textColor: Color { selectedTheme.currentTheme.labelColor } var body: some View { - ZStack { - selectedTheme.currentTheme.secondaryBGColor - VStack { - HStack(spacing: 0) { - themeButton(for: .system) - themeButton(for: .iFeel) - themeButton(for: .dark) - themeButton(for: .light) - } - .padding(.top) + VStack { + HStack(spacing: 0) { + themeButton(for: .system) + themeButton(for: .iFeel) + themeButton(for: .dark) + themeButton(for: .light) } - .padding() + .padding(.top) } + .padding() + .background(selectedTheme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .onAppear { diff --git a/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift b/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift index ab5eb15..08c453a 100644 --- a/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift @@ -18,57 +18,54 @@ struct VotingLayoutPickerView: View { } var body: some View { - ZStack { - theme.currentTheme.secondaryBGColor + VStack(alignment: .leading, spacing: 12) { + Text("Voting Layout") + .font(.headline) + .foregroundColor(textColor) + .padding(.horizontal) + .padding(.top) - VStack(alignment: .leading, spacing: 12) { - Text("Voting Layout") - .font(.headline) - .foregroundColor(textColor) - .padding(.horizontal) - .padding(.top) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in - Button(action: { - if UIAccessibility.isReduceMotionEnabled { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in + Button(action: { + if UIAccessibility.isReduceMotionEnabled { + votingLayoutStyle = layout.rawValue + } else { + withAnimation(.easeInOut(duration: 0.2)) { votingLayoutStyle = layout.rawValue - } else { - withAnimation(.easeInOut(duration: 0.2)) { - votingLayoutStyle = layout.rawValue - } } - AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName)) - }) { - VStack(spacing: 6) { - layoutIcon(for: layout) - .frame(width: 44, height: 44) - .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6)) - - Text(layout.displayName) - .font(.caption) - .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8)) - } - .frame(width: 70) - .padding(.vertical, 12) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear) - ) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2) - ) } - .buttonStyle(.plain) + AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName)) + }) { + VStack(spacing: 6) { + layoutIcon(for: layout) + .frame(width: 44, height: 44) + .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6)) + + Text(layout.displayName) + .font(.caption) + .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8)) + } + .frame(width: 70) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear) + ) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2) + ) } + .buttonStyle(.plain) } - .padding(.horizontal) } - .padding(.bottom) + .padding(.horizontal) } + .padding(.bottom) } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index e5b4d9d..e2fb606 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -118,39 +118,37 @@ struct SettingsContentView: View { // MARK: - Reminder Time Button private var reminderTimeButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.reminderTimeTapped) - showReminderTimePicker = true - }, label: { - HStack(spacing: 12) { - Image(systemName: "clock.fill") - .font(.title2) - .foregroundColor(.orange) - .frame(width: 32) + Button(action: { + AnalyticsManager.shared.track(.reminderTimeTapped) + showReminderTimePicker = true + }, label: { + HStack(spacing: 12) { + Image(systemName: "clock.fill") + .font(.title2) + .foregroundColor(.orange) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Reminder Time") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Reminder Time") + .foregroundColor(textColor) - Text(formattedReminderTime) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text(formattedReminderTime) .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() - }) - .accessibilityLabel(String(localized: "Reminder Time")) - .accessibilityValue(formattedReminderTime) - .accessibilityHint(String(localized: "Opens time picker to change reminder time")) - } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() + }) + .accessibilityLabel(String(localized: "Reminder Time")) + .accessibilityValue(formattedReminderTime) + .accessibilityHint(String(localized: "Opens time picker to change reminder time")) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -217,63 +215,59 @@ struct SettingsContentView: View { } private var bypassSubscriptionToggle: some View { - ZStack { - theme.currentTheme.secondaryBGColor + HStack(spacing: 12) { + Image(systemName: "lock.open.fill") + .font(.title2) + .foregroundColor(.green) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Bypass Subscription") + .foregroundColor(textColor) + + Text("Hide trial banner & grant full access") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Toggle("", isOn: $iapManager.bypassSubscription) + .labelsHidden() + } + .padding() + .background(theme.currentTheme.secondaryBGColor) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } + + private var trialDateButton: some View { + VStack(spacing: 12) { HStack(spacing: 12) { - Image(systemName: "lock.open.fill") + Image(systemName: "calendar.badge.clock") .font(.title2) - .foregroundColor(.green) + .foregroundColor(.orange) .frame(width: 32) VStack(alignment: .leading, spacing: 2) { - Text("Bypass Subscription") + Text("Trial Start Date") .foregroundColor(textColor) - Text("Hide trial banner & grant full access") + Text("Current: \(firstLaunchDate.formatted(date: .abbreviated, time: .omitted))") .font(.caption) .foregroundStyle(.secondary) } Spacer() - Toggle("", isOn: $iapManager.bypassSubscription) - .labelsHidden() + Button("Change") { + showTrialDatePicker = true + } + .font(.subheadline.weight(.medium)) } .padding() } - .fixedSize(horizontal: false, vertical: true) - .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) - } - - private var trialDateButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack(spacing: 12) { - HStack(spacing: 12) { - Image(systemName: "calendar.badge.clock") - .font(.title2) - .foregroundColor(.orange) - .frame(width: 32) - - VStack(alignment: .leading, spacing: 2) { - Text("Trial Start Date") - .foregroundColor(textColor) - - Text("Current: \(firstLaunchDate.formatted(date: .abbreviated, time: .omitted))") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Button("Change") { - showTrialDatePicker = true - } - .font(.subheadline.weight(.medium)) - } - .padding() - } - } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .sheet(isPresented: $showTrialDatePicker) { @@ -308,35 +302,33 @@ struct SettingsContentView: View { @State private var showTipsPreview = false private var animationLabButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - showAnimationLab = true - } label: { - HStack(spacing: 12) { - Image(systemName: "sparkles") - .font(.title2) - .foregroundColor(.purple) - .frame(width: 32) + Button { + showAnimationLab = true + } label: { + HStack(spacing: 12) { + Image(systemName: "sparkles") + .font(.title2) + .foregroundColor(.purple) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Animation Lab") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Animation Lab") + .foregroundColor(textColor) - Text("Experiment with vote celebrations") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text("Experiment with vote celebrations") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .sheet(isPresented: $showAnimationLab) { @@ -347,41 +339,39 @@ struct SettingsContentView: View { } private var paywallPreviewButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - showPaywallPreview = true - } label: { - HStack(spacing: 12) { - Image(systemName: "paintpalette.fill") - .font(.title2) - .foregroundStyle( - LinearGradient( - colors: [.purple, .pink, .orange], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + Button { + showPaywallPreview = true + } label: { + HStack(spacing: 12) { + Image(systemName: "paintpalette.fill") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.purple, .pink, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing ) - .frame(width: 32) + ) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Paywall Styles") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Paywall Styles") + .foregroundColor(textColor) - Text("Preview subscription themes") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text("Preview subscription themes") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .sheet(isPresented: $showPaywallPreview) { @@ -392,41 +382,39 @@ struct SettingsContentView: View { } private var tipsPreviewButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - showTipsPreview = true - } label: { - HStack(spacing: 12) { - Image(systemName: "lightbulb.fill") - .font(.title2) - .foregroundStyle( - LinearGradient( - colors: [.yellow, .orange], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + Button { + showTipsPreview = true + } label: { + HStack(spacing: 12) { + Image(systemName: "lightbulb.fill") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.yellow, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing ) - .frame(width: 32) + ) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Tips Preview") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Tips Preview") + .foregroundColor(textColor) - Text("View all tip modals") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text("View all tip modals") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .sheet(isPresented: $showTipsPreview) { @@ -437,400 +425,384 @@ struct SettingsContentView: View { } private var testNotificationsButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - LocalNotification.sendAllPersonalityNotificationsForScreenshot() - } label: { - HStack(spacing: 12) { - Image(systemName: "bell.badge.fill") - .font(.title2) - .foregroundColor(.red) - .frame(width: 32) + Button { + LocalNotification.sendAllPersonalityNotificationsForScreenshot() + } label: { + HStack(spacing: 12) { + Image(systemName: "bell.badge.fill") + .font(.title2) + .foregroundColor(.red) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Test All Notifications") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Test All Notifications") + .foregroundColor(textColor) - Text("Send 5 personality pack notifications") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "arrow.up.right") + Text("Send 5 personality pack notifications") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Image(systemName: "arrow.up.right") + .font(.caption) + .foregroundStyle(.tertiary) } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportWidgetsButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isExportingWidgets = true - Task { - widgetExportPath = await WidgetExporter.exportAllWidgets() - isExportingWidgets = false - if let path = widgetExportPath { - print("๐Ÿ“ธ Widgets exported to: \(path.path)") - openInFilesApp(path) - } + Button { + isExportingWidgets = true + Task { + widgetExportPath = await WidgetExporter.exportAllWidgets() + isExportingWidgets = false + if let path = widgetExportPath { + print("๐Ÿ“ธ Widgets exported to: \(path.path)") + openInFilesApp(path) } - } label: { - HStack(spacing: 12) { - if isExportingWidgets { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "square.grid.2x2.fill") - .font(.title2) - .foregroundColor(.purple) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Export Widget Screenshots") - .foregroundColor(textColor) - - if let path = widgetExportPath { - Text("Saved to Documents/WidgetExports") - .font(.caption) - .foregroundColor(.green) - } else { - Text("Light & dark mode PNGs") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "arrow.down.doc.fill") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding() } - .disabled(isExportingWidgets) + } label: { + HStack(spacing: 12) { + if isExportingWidgets { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "square.grid.2x2.fill") + .font(.title2) + .foregroundColor(.purple) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Export Widget Screenshots") + .foregroundColor(textColor) + + if let path = widgetExportPath { + Text("Saved to Documents/WidgetExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("Light & dark mode PNGs") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() } + .disabled(isExportingWidgets) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportVotingLayoutsButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isExportingVotingLayouts = true - Task { - votingLayoutExportPath = await WidgetExporter.exportAllVotingLayouts() - isExportingVotingLayouts = false - if let path = votingLayoutExportPath { - print("๐Ÿ“ธ Voting layouts exported to: \(path.path)") - openInFilesApp(path) - } + Button { + isExportingVotingLayouts = true + Task { + votingLayoutExportPath = await WidgetExporter.exportAllVotingLayouts() + isExportingVotingLayouts = false + if let path = votingLayoutExportPath { + print("๐Ÿ“ธ Voting layouts exported to: \(path.path)") + openInFilesApp(path) } - } label: { - HStack(spacing: 12) { - if isExportingVotingLayouts { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "hand.tap.fill") - .font(.title2) - .foregroundColor(.blue) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Export Voting Layouts") - .foregroundColor(textColor) - - if let path = votingLayoutExportPath { - Text("Saved to Documents/VotingLayoutExports") - .font(.caption) - .foregroundColor(.green) - } else { - Text("All sizes & theme variations") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "arrow.down.doc.fill") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding() } - .disabled(isExportingVotingLayouts) + } label: { + HStack(spacing: 12) { + if isExportingVotingLayouts { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "hand.tap.fill") + .font(.title2) + .foregroundColor(.blue) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Export Voting Layouts") + .foregroundColor(textColor) + + if let path = votingLayoutExportPath { + Text("Saved to Documents/VotingLayoutExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("All sizes & theme variations") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() } + .disabled(isExportingVotingLayouts) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportWatchViewsButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isExportingWatchViews = true - Task { - watchExportPath = await WatchExporter.exportAllWatchViews() - isExportingWatchViews = false - if let path = watchExportPath { - print("โŒš Watch views exported to: \(path.path)") - openInFilesApp(path) - } + Button { + isExportingWatchViews = true + Task { + watchExportPath = await WatchExporter.exportAllWatchViews() + isExportingWatchViews = false + if let path = watchExportPath { + print("โŒš Watch views exported to: \(path.path)") + openInFilesApp(path) } - } label: { - HStack(spacing: 12) { - if isExportingWatchViews { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "applewatch.watchface") - .font(.title2) - .foregroundColor(.cyan) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Export Watch Screenshots") - .foregroundColor(textColor) - - if let path = watchExportPath { - Text("Saved to Documents/WatchExports") - .font(.caption) - .foregroundColor(.green) - } else { - Text("All styles & complications") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "arrow.down.doc.fill") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding() } - .disabled(isExportingWatchViews) + } label: { + HStack(spacing: 12) { + if isExportingWatchViews { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "applewatch.watchface") + .font(.title2) + .foregroundColor(.cyan) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Export Watch Screenshots") + .foregroundColor(textColor) + + if let path = watchExportPath { + Text("Saved to Documents/WatchExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("All styles & complications") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() } + .disabled(isExportingWatchViews) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportInsightsButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isExportingInsights = true - Task { - insightsExportPath = await InsightsExporter.exportInsightsScreenshots() - isExportingInsights = false - if let path = insightsExportPath { - print("โœจ Insights exported to: \(path.path)") - openInFilesApp(path) - } + Button { + isExportingInsights = true + Task { + insightsExportPath = await InsightsExporter.exportInsightsScreenshots() + isExportingInsights = false + if let path = insightsExportPath { + print("โœจ Insights exported to: \(path.path)") + openInFilesApp(path) } - } label: { - HStack(spacing: 12) { - if isExportingInsights { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "sparkles") - .font(.title2) - .foregroundStyle( - LinearGradient( - colors: [.purple, .blue], - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Export Insights Screenshots") - .foregroundColor(textColor) - - if let path = insightsExportPath { - Text("Saved to Documents/InsightsExports") - .font(.caption) - .foregroundColor(.green) - } else { - Text("AI insights in light & dark mode") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "arrow.down.doc.fill") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding() } - .disabled(isExportingInsights) + } label: { + HStack(spacing: 12) { + if isExportingInsights { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "sparkles") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.purple, .blue], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Export Insights Screenshots") + .foregroundColor(textColor) + + if let path = insightsExportPath { + Text("Saved to Documents/InsightsExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("AI insights in light & dark mode") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() } + .disabled(isExportingInsights) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var generateAndExportButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isGeneratingScreenshots = true - Task { - DataController.shared.populate2YearsData() - sharingExportPath = await SharingScreenshotExporter.exportAllSharingScreenshots() - isGeneratingScreenshots = false - if let path = sharingExportPath { - print("๐Ÿ“ธ Sharing screenshots exported to: \(path.path)") - openInFilesApp(path) - } + Button { + isGeneratingScreenshots = true + Task { + DataController.shared.populate2YearsData() + sharingExportPath = await SharingScreenshotExporter.exportAllSharingScreenshots() + isGeneratingScreenshots = false + if let path = sharingExportPath { + print("๐Ÿ“ธ Sharing screenshots exported to: \(path.path)") + openInFilesApp(path) } - } label: { - HStack(spacing: 12) { - if isGeneratingScreenshots { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "photo.on.rectangle.angled") - .font(.title2) - .foregroundStyle( - LinearGradient( - colors: [.green, .blue], - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Generate & Export Sharing") - .foregroundColor(textColor) - - if let path = sharingExportPath { - Text("Saved to Documents/SharingExports") - .font(.caption) - .foregroundColor(.green) - } else { - Text("Fill 2 years data + export PNGs") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "arrow.down.doc.fill") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding() } - .disabled(isGeneratingScreenshots) + } label: { + HStack(spacing: 12) { + if isGeneratingScreenshots { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "photo.on.rectangle.angled") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.green, .blue], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Generate & Export Sharing") + .foregroundColor(textColor) + + if let path = sharingExportPath { + Text("Saved to Documents/SharingExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("Fill 2 years data + export PNGs") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() } + .disabled(isGeneratingScreenshots) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var deleteHealthKitDataButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - isDeletingHealthKitData = true - healthKitDeleteResult = nil - Task { - do { - let count = try await HealthKitManager.shared.deleteAllMoods() - healthKitDeleteResult = "โœ“ Deleted \(count) records" - } catch { - healthKitDeleteResult = "โœ— Error: \(error.localizedDescription)" - } - isDeletingHealthKitData = false + Button { + isDeletingHealthKitData = true + healthKitDeleteResult = nil + Task { + do { + let count = try await HealthKitManager.shared.deleteAllMoods() + healthKitDeleteResult = "โœ“ Deleted \(count) records" + } catch { + healthKitDeleteResult = "โœ— Error: \(error.localizedDescription)" } - } label: { - HStack(spacing: 12) { - if isDeletingHealthKitData { - ProgressView() - .frame(width: 32) - } else { - Image(systemName: "heart.slash.fill") - .font(.title2) - .foregroundColor(.red) - .frame(width: 32) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Delete HealthKit Data") - .foregroundColor(textColor) - - if let result = healthKitDeleteResult { - Text(result) - .font(.caption) - .foregroundColor(result.contains("โœ“") ? .green : .red) - } else { - Text("Remove all State of Mind records") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Spacer() - } - .padding() + isDeletingHealthKitData = false } - .disabled(isDeletingHealthKitData) + } label: { + HStack(spacing: 12) { + if isDeletingHealthKitData { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "heart.slash.fill") + .font(.title2) + .foregroundColor(.red) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Delete HealthKit Data") + .foregroundColor(textColor) + + if let result = healthKitDeleteResult { + Text(result) + .font(.caption) + .foregroundColor(result.contains("โœ“") ? .green : .red) + } else { + Text("Remove all State of Mind records") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + } + .padding() } + .disabled(isDeletingHealthKitData) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var clearDataButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - MoodLogger.shared.deleteAllData() - } label: { - HStack(spacing: 12) { - Image(systemName: "trash") - .font(.title2) - .foregroundColor(.red) - .frame(width: 32) + Button { + MoodLogger.shared.deleteAllData() + } label: { + HStack(spacing: 12) { + Image(systemName: "trash") + .font(.title2) + .foregroundColor(.red) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Clear All Data") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Clear All Data") + .foregroundColor(textColor) - Text("Delete all mood entries") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() + Text("Delete all mood entries") + .font(.caption) + .foregroundStyle(.secondary) } - .padding() + + Spacer() } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .accessibilityIdentifier(AccessibilityID.Settings.clearDataButton) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) @@ -842,46 +814,44 @@ struct SettingsContentView: View { @ViewBuilder private var privacyLockToggle: some View { if authManager.canUseBiometrics { - ZStack { - theme.currentTheme.secondaryBGColor - HStack(spacing: 12) { - Image(systemName: authManager.biometricIcon) - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + HStack(spacing: 12) { + Image(systemName: authManager.biometricIcon) + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Privacy Lock") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Privacy Lock") + .foregroundColor(textColor) - Text("Require \(authManager.biometricName) to open app") - .font(.caption) - .foregroundStyle(.secondary) - } + Text("Require \(authManager.biometricName) to open app") + .font(.caption) + .foregroundStyle(.secondary) + } - Spacer() + Spacer() - Toggle("", isOn: Binding( - get: { authManager.isLockEnabled }, - set: { newValue in - Task { - if newValue { - let success = await authManager.enableLock() - if !success { - AnalyticsManager.shared.track(.privacyLockEnableFailed) - } - } else { - authManager.disableLock() + Toggle("", isOn: Binding( + get: { authManager.isLockEnabled }, + set: { newValue in + Task { + if newValue { + let success = await authManager.enableLock() + if !success { + AnalyticsManager.shared.track(.privacyLockEnableFailed) } + } else { + authManager.disableLock() } } - )) - .labelsHidden() - .accessibilityLabel(String(localized: "Privacy Lock")) - .accessibilityHint(String(localized: "Require biometric authentication to open app")) - } - .padding() + } + )) + .labelsHidden() + .accessibilityLabel(String(localized: "Privacy Lock")) + .accessibilityHint(String(localized: "Require biometric authentication to open app")) } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -892,31 +862,29 @@ struct SettingsContentView: View { @ObservedObject private var healthKitManager = HealthKitManager.shared private var addTestDataButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button { - DataController.shared.populateTestData() - } label: { - HStack(spacing: 12) { - Image(systemName: "plus.square.on.square") - .font(.title2) - .foregroundColor(.green) - .frame(width: 32) - - VStack(alignment: .leading, spacing: 2) { - Text("Add Test Data") - .foregroundColor(textColor) - - Text("Populate with sample mood entries") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() + Button { + DataController.shared.populateTestData() + } label: { + HStack(spacing: 12) { + Image(systemName: "plus.square.on.square") + .font(.title2) + .foregroundColor(.green) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Add Test Data") + .foregroundColor(textColor) + + Text("Populate with sample mood entries") + .font(.caption) + .foregroundStyle(.secondary) } - .padding() + + Spacer() } + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -1031,112 +999,105 @@ struct SettingsContentView: View { // MARK: - Export Data Button private var exportDataButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.exportTapped) - showExportView = true - }, label: { - HStack(spacing: 12) { - Image(systemName: "square.and.arrow.up") - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + Button(action: { + AnalyticsManager.shared.track(.exportTapped) + showExportView = true + }, label: { + HStack(spacing: 12) { + Image(systemName: "square.and.arrow.up") + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Export Data") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Export Data") + .foregroundColor(textColor) - Text("CSV or PDF report") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text("CSV or PDF report") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() - }) - .accessibilityLabel(String(localized: "Export Data")) - .accessibilityHint(String(localized: "Export your mood data as CSV or PDF")) - } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() + }) + .accessibilityLabel(String(localized: "Export Data")) + .accessibilityHint(String(localized: "Export your mood data as CSV or PDF")) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var showOnboardingButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.onboardingReshown) - showOnboarding.toggle() - }, label: { - Text(String(localized: "settings_view_show_onboarding")) - .foregroundColor(textColor) - }) - .accessibilityHint(String(localized: "View the app introduction again")) - .accessibilityIdentifier(AccessibilityID.Settings.showOnboardingButton) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.onboardingReshown) + showOnboarding.toggle() + }, label: { + Text(String(localized: "settings_view_show_onboarding")) + .foregroundColor(textColor) + }) + .accessibilityHint(String(localized: "View the app introduction again")) + .accessibilityIdentifier(AccessibilityID.Settings.showOnboardingButton) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var canDelete: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - Toggle(String(localized: "settings_use_delete_enable"), - isOn: $deleteEnabled) - .onChange(of: deleteEnabled) { _, newValue in - AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue)) - } - .foregroundColor(textColor) - .accessibilityHint(String(localized: "Allow deleting mood entries by swiping")) - .padding() + VStack { + Toggle(String(localized: "settings_use_delete_enable"), + isOn: $deleteEnabled) + .onChange(of: deleteEnabled) { _, newValue in + AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue)) } + .foregroundColor(textColor) + .accessibilityHint(String(localized: "Allow deleting mood entries by swiping")) + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var eulaButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.eulaViewed) - if let url = URL(string: "https://feels.88oakapps.com/eula.html") { - UIApplication.shared.open(url) - } - }, label: { - Text(String(localized: "settings_view_show_eula")) - .foregroundColor(textColor) - }) - .accessibilityHint(String(localized: "Opens End User License Agreement in browser")) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.eulaViewed) + if let url = URL(string: "https://feels.88oakapps.com/eula.html") { + UIApplication.shared.open(url) + } + }, label: { + Text(String(localized: "settings_view_show_eula")) + .foregroundColor(textColor) + }) + .accessibilityHint(String(localized: "Opens End User License Agreement in browser")) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var privacyButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.privacyPolicyViewed) - if let url = URL(string: "https://feels.88oakapps.com/privacy.html") { - UIApplication.shared.open(url) - } - }, label: { - Text(String(localized: "settings_view_show_privacy")) - .foregroundColor(textColor) - }) - .accessibilityHint(String(localized: "Opens Privacy Policy in browser")) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.privacyPolicyViewed) + if let url = URL(string: "https://feels.88oakapps.com/privacy.html") { + UIApplication.shared.open(url) + } + }, label: { + Text(String(localized: "settings_view_show_privacy")) + .foregroundColor(textColor) + }) + .accessibilityHint(String(localized: "Opens Privacy Policy in browser")) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -1144,42 +1105,40 @@ struct SettingsContentView: View { // MARK: - Analytics Toggle private var analyticsToggle: some View { - ZStack { - theme.currentTheme.secondaryBGColor - HStack(spacing: 12) { - Image(systemName: "chart.bar.xaxis") - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + HStack(spacing: 12) { + Image(systemName: "chart.bar.xaxis") + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Share Analytics") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Share Analytics") + .foregroundColor(textColor) - Text("Help improve Feels by sharing anonymous usage data") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Toggle("", isOn: Binding( - get: { !AnalyticsManager.shared.isOptedOut }, - set: { enabled in - if enabled { - AnalyticsManager.shared.optIn() - } else { - AnalyticsManager.shared.optOut() - } - } - )) - .labelsHidden() - .accessibilityIdentifier(AccessibilityID.Settings.analyticsToggle) - .accessibilityLabel("Share Analytics") - .accessibilityHint("Toggle anonymous usage analytics") + Text("Help improve Feels by sharing anonymous usage data") + .font(.caption) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Toggle("", isOn: Binding( + get: { !AnalyticsManager.shared.isOptedOut }, + set: { enabled in + if enabled { + AnalyticsManager.shared.optIn() + } else { + AnalyticsManager.shared.optOut() + } + } + )) + .labelsHidden() + .accessibilityIdentifier(AccessibilityID.Settings.analyticsToggle) + .accessibilityLabel("Share Analytics") + .accessibilityHint("Toggle anonymous usage analytics") } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -1458,44 +1417,42 @@ struct SettingsView: View { @ViewBuilder private var privacyLockToggle: some View { if authManager.canUseBiometrics { - ZStack { - theme.currentTheme.secondaryBGColor - HStack(spacing: 12) { - Image(systemName: authManager.biometricIcon) - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + HStack(spacing: 12) { + Image(systemName: authManager.biometricIcon) + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Privacy Lock") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Privacy Lock") + .foregroundColor(textColor) - Text("Require \(authManager.biometricName) to open app") - .font(.caption) - .foregroundStyle(.secondary) - } + Text("Require \(authManager.biometricName) to open app") + .font(.caption) + .foregroundStyle(.secondary) + } - Spacer() + Spacer() - Toggle("", isOn: Binding( - get: { authManager.isLockEnabled }, - set: { newValue in - Task { - if newValue { - let success = await authManager.enableLock() - if !success { - AnalyticsManager.shared.track(.privacyLockEnableFailed) - } - } else { - authManager.disableLock() + Toggle("", isOn: Binding( + get: { authManager.isLockEnabled }, + set: { newValue in + Task { + if newValue { + let success = await authManager.enableLock() + if !success { + AnalyticsManager.shared.track(.privacyLockEnableFailed) } + } else { + authManager.disableLock() } } - )) - .labelsHidden() - } - .padding() + } + )) + .labelsHidden() } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -1607,36 +1564,34 @@ struct SettingsView: View { // MARK: - Export Data Button private var exportDataButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.exportTapped) - showExportView = true - }, label: { - HStack(spacing: 12) { - Image(systemName: "square.and.arrow.up") - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + Button(action: { + AnalyticsManager.shared.track(.exportTapped) + showExportView = true + }, label: { + HStack(spacing: 12) { + Image(systemName: "square.and.arrow.up") + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Export Data") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Export Data") + .foregroundColor(textColor) - Text("CSV or PDF report") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") + Text("CSV or PDF report") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(.secondary) } - .padding() - }) - } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() + }) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } @@ -1656,80 +1611,75 @@ struct SettingsView: View { } private var specialThanksCell: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - Button(action: { - AnalyticsManager.shared.track(.specialThanksViewed) - withAnimation{ - showSpecialThanks.toggle() - } - }, label: { - Text(String(localized: "settings_view_special_thanks_to_title")) - .foregroundColor(textColor) - }) - .padding() - - if showSpecialThanks { - Divider() - Link("Font Awesome", destination: URL(string: "https://fontawesome.com")!) - .accentColor(textColor) - .padding(.bottom) - - Divider() - - Link("Charts", destination: URL(string: "https://github.com/danielgindi/Charts")!) - .accentColor(textColor) - .padding(.bottom) + VStack { + Button(action: { + AnalyticsManager.shared.track(.specialThanksViewed) + withAnimation{ + showSpecialThanks.toggle() } + }, label: { + Text(String(localized: "settings_view_special_thanks_to_title")) + .foregroundColor(textColor) + }) + .padding() + + if showSpecialThanks { + Divider() + Link("Font Awesome", destination: URL(string: "https://fontawesome.com")!) + .accentColor(textColor) + .padding(.bottom) + + Divider() + + Link("Charts", destination: URL(string: "https://github.com/danielgindi/Charts")!) + .accentColor(textColor) + .padding(.bottom) } } + .background(theme.currentTheme.secondaryBGColor) .frame(minWidth: 0, maxWidth: .infinity) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var addTestDataCell: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - DataController.shared.populateTestData() - }, label: { - Text("Add test data") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + DataController.shared.populateTestData() + }, label: { + Text("Add test data") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var editFirstLaunchDatePast: some View { - ZStack { - theme.currentTheme.secondaryBGColor - HStack(spacing: 12) { - Image(systemName: "calendar.badge.clock") - .font(.title2) - .foregroundColor(.orange) - .frame(width: 32) + HStack(spacing: 12) { + Image(systemName: "calendar.badge.clock") + .font(.title2) + .foregroundColor(.orange) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Trial Start Date") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Trial Start Date") + .foregroundColor(textColor) - Text("Current: \(firstLaunchDate.formatted(date: .abbreviated, time: .omitted))") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Button("Change") { - showTrialDatePicker = true - } - .font(.subheadline.weight(.medium)) + Text("Current: \(firstLaunchDate.formatted(date: .abbreviated, time: .omitted))") + .font(.caption) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Button("Change") { + showTrialDatePicker = true + } + .font(.subheadline.weight(.medium)) } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .sheet(isPresented: $showTrialDatePicker) { @@ -1760,220 +1710,204 @@ struct SettingsView: View { } private var resetLaunchDate: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - firstLaunchDate = Date() - Task { - await iapManager.checkSubscriptionStatus() - } - }, label: { - Text("Reset luanch date to current date") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + firstLaunchDate = Date() + Task { + await iapManager.checkSubscriptionStatus() + } + }, label: { + Text("Reset luanch date to current date") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var clearDB: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - MoodLogger.shared.deleteAllData() - }, label: { - Text("Clear DB") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + MoodLogger.shared.deleteAllData() + }, label: { + Text("Clear DB") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var fixWeekday: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - DataController.shared.fixWrongWeekdays() - }, label: { - Text("Fix Weekday") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + DataController.shared.fixWrongWeekdays() + }, label: { + Text("Fix Weekday") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var whyBackgroundMode: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - Button(action: { - withAnimation{ - showWhyBGMode.toggle() - } - }, label: { - Text(String(localized: "settings_view_why_bg_mode_title")) - .foregroundColor(textColor) - }) - .padding() - if showWhyBGMode { - Text(String(localized: "settings_view_why_bg_mode_body")) - .foregroundColor(textColor) - .padding() + VStack { + Button(action: { + withAnimation{ + showWhyBGMode.toggle() } + }, label: { + Text(String(localized: "settings_view_why_bg_mode_title")) + .foregroundColor(textColor) + }) + .padding() + if showWhyBGMode { + Text(String(localized: "settings_view_why_bg_mode_body")) + .foregroundColor(textColor) + .padding() } } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var showOnboardingButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.onboardingReshown) - showOnboarding.toggle() - }, label: { - Text(String(localized: "settings_view_show_onboarding")) - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.onboardingReshown) + showOnboarding.toggle() + }, label: { + Text(String(localized: "settings_view_show_onboarding")) + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var eulaButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.eulaViewed) - openURL(URL(string: "https://feels.88oakapps.com/eula.html")!) - }, label: { - Text(String(localized: "settings_view_show_eula")) - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.eulaViewed) + openURL(URL(string: "https://feels.88oakapps.com/eula.html")!) + }, label: { + Text(String(localized: "settings_view_show_eula")) + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var privacyButton: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - AnalyticsManager.shared.track(.privacyPolicyViewed) - openURL(URL(string: "https://feels.88oakapps.com/privacy.html")!) - }, label: { - Text(String(localized: "settings_view_show_privacy")) - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + AnalyticsManager.shared.track(.privacyPolicyViewed) + openURL(URL(string: "https://feels.88oakapps.com/privacy.html")!) + }, label: { + Text(String(localized: "settings_view_show_privacy")) + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var analyticsToggle: some View { - ZStack { - theme.currentTheme.secondaryBGColor - HStack(spacing: 12) { - Image(systemName: "chart.bar.xaxis") - .font(.title2) - .foregroundColor(.accentColor) - .frame(width: 32) + HStack(spacing: 12) { + Image(systemName: "chart.bar.xaxis") + .font(.title2) + .foregroundColor(.accentColor) + .frame(width: 32) - VStack(alignment: .leading, spacing: 2) { - Text("Share Analytics") - .foregroundColor(textColor) + VStack(alignment: .leading, spacing: 2) { + Text("Share Analytics") + .foregroundColor(textColor) - Text("Help improve Feels by sharing anonymous usage data") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Toggle("", isOn: Binding( - get: { !AnalyticsManager.shared.isOptedOut }, - set: { enabled in - if enabled { - AnalyticsManager.shared.optIn() - } else { - AnalyticsManager.shared.optOut() - } - } - )) - .labelsHidden() - .accessibilityIdentifier(AccessibilityID.Settings.analyticsToggle) - .accessibilityLabel("Share Analytics") - .accessibilityHint("Toggle anonymous usage analytics") + Text("Help improve Feels by sharing anonymous usage data") + .font(.caption) + .foregroundStyle(.secondary) } - .padding() + + Spacer() + + Toggle("", isOn: Binding( + get: { !AnalyticsManager.shared.isOptedOut }, + set: { enabled in + if enabled { + AnalyticsManager.shared.optIn() + } else { + AnalyticsManager.shared.optOut() + } + } + )) + .labelsHidden() + .accessibilityIdentifier(AccessibilityID.Settings.analyticsToggle) + .accessibilityLabel("Share Analytics") + .accessibilityHint("Toggle anonymous usage analytics") } + .padding() + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var canDelete: some View { - ZStack { - theme.currentTheme.secondaryBGColor - VStack { - Toggle(String(localized: "settings_use_delete_enable"), - isOn: $deleteEnabled) - .onChange(of: deleteEnabled) { _, newValue in - AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue)) - } - .foregroundColor(textColor) - .padding() + VStack { + Toggle(String(localized: "settings_use_delete_enable"), + isOn: $deleteEnabled) + .onChange(of: deleteEnabled) { _, newValue in + AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue)) } + .foregroundColor(textColor) + .padding() } + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var exportData: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - showingExporter.toggle() - AnalyticsManager.shared.track(.exportTapped) - }, label: { - Text("Export") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + showingExporter.toggle() + AnalyticsManager.shared.track(.exportTapped) + }, label: { + Text("Export") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var importData: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - showingImporter.toggle() - AnalyticsManager.shared.track(.importTapped) - }, label: { - Text("Import") - .foregroundColor(textColor) - }) - .padding() - } + Button(action: { + showingImporter.toggle() + AnalyticsManager.shared.track(.importTapped) + }, label: { + Text("Import") + .foregroundColor(textColor) + }) + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } private var randomIcons: some View { - ZStack { - theme.currentTheme.secondaryBGColor - Button(action: { - var iconViews = [UIImage]() + Button(action: { + var iconViews = [UIImage]() // for _ in 0...300 { // iconViews.append( @@ -2094,12 +2028,13 @@ struct SettingsView: View { Text("Create random icons") .foregroundColor(textColor) }) - .padding() - } + .padding() + .frame(maxWidth: .infinity) + .background(theme.currentTheme.secondaryBGColor) .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } - + } struct TextFile: FileDocument { diff --git a/Shared/Views/YearView/YearView.swift b/Shared/Views/YearView/YearView.swift index 6eb62f5..1eea40a 100644 --- a/Shared/Views/YearView/YearView.swift +++ b/Shared/Views/YearView/YearView.swift @@ -522,6 +522,7 @@ struct YearCard: View, Equatable { } } .buttonStyle(.plain) + .accessibilityIdentifier(AccessibilityID.YearView.cardHeader(year: year)) Spacer() @@ -533,6 +534,7 @@ struct YearCard: View, Equatable { .foregroundColor(textColor.opacity(0.6)) } .buttonStyle(.plain) + .accessibilityIdentifier(AccessibilityID.YearView.shareButton) } .padding(.horizontal, 16) .padding(.vertical, 12) @@ -543,6 +545,7 @@ struct YearCard: View, Equatable { // Donut Chart MoodDonutChart(metrics: animatedMetrics, moodTint: moodTint) .frame(width: 100, height: 100) + .accessibilityIdentifier(AccessibilityID.YearView.donutChart) // Bar Chart VStack(spacing: 6) { @@ -574,10 +577,12 @@ struct YearCard: View, Equatable { } } .frame(maxWidth: .infinity) + .accessibilityIdentifier(AccessibilityID.YearView.barChart) } .padding(.horizontal, 16) .padding(.bottom, 12) .transition(.opacity.combined(with: .move(edge: .top))) + .accessibilityIdentifier(AccessibilityID.YearView.statsSection) } Divider() diff --git a/Tests iOS/Helpers/WaitHelpers.swift b/Tests iOS/Helpers/WaitHelpers.swift index 738f47a..7c20bb3 100644 --- a/Tests iOS/Helpers/WaitHelpers.swift +++ b/Tests iOS/Helpers/WaitHelpers.swift @@ -82,6 +82,14 @@ enum UITestID { static let header = "insights_header" } + enum Year { + static let donutChart = "year_donut_chart" + static let barChart = "year_bar_chart" + static let statsSection = "year_stats_section" + static func cardHeader(year: Int) -> String { "year_card_header_\(year)" } + static let shareButton = "year_share_button" + } + enum Month { static let grid = "month_grid" } diff --git a/Tests iOS/SettingsOnboardingTests.swift b/Tests iOS/SettingsOnboardingTests.swift new file mode 100644 index 0000000..d484799 --- /dev/null +++ b/Tests iOS/SettingsOnboardingTests.swift @@ -0,0 +1,48 @@ +// +// SettingsOnboardingTests.swift +// Tests iOS +// +// TC-124: Show onboarding from Settings. +// + +import XCTest + +final class SettingsOnboardingTests: BaseUITestCase { + override var seedFixture: String? { "empty" } + override var bypassSubscription: Bool { true } + + /// TC-124: Tapping Show Onboarding in Settings replays the onboarding flow. + func testSettings_ShowOnboarding_OpensOnboardingFlow() { + let tabBar = TabBarScreen(app: app) + let settingsScreen = tabBar.tapSettings() + settingsScreen.assertVisible() + + // Switch to the Settings sub-tab (not Customize) + settingsScreen.tapSettingsTab() + + captureScreenshot(name: "settings_before_show_onboarding") + + // Scroll to and tap "Show Onboarding" button + let showOnboardingBtn = app.element("settings_show_onboarding") + guard showOnboardingBtn.waitForExistence(timeout: 2) || + app.swipeUntilExists(showOnboardingBtn, direction: .up, maxSwipes: 8) else { + captureScreenshot(name: "settings_show_onboarding_not_found") + XCTFail("Show Onboarding button not found in Settings") + return + } + showOnboardingBtn.tapWhenReady() + + // The sheet may take a moment to animate in. + // Look for the onboarding welcome screen or any text unique to onboarding. + let welcomeScreen = app.element(UITestID.Onboarding.welcome) + let welcomeText = app.staticTexts["Welcome to Feels"] + let found = welcomeScreen.waitForExistence(timeout: 8) || + welcomeText.waitForExistence(timeout: 3) + + captureScreenshot(name: "settings_show_onboarding_result") + + XCTAssertTrue(found, + "Onboarding should appear after tapping Show Onboarding" + ) + } +} diff --git a/Tests iOS/YearViewDisplayTests.swift b/Tests iOS/YearViewDisplayTests.swift new file mode 100644 index 0000000..5e8853d --- /dev/null +++ b/Tests iOS/YearViewDisplayTests.swift @@ -0,0 +1,51 @@ +// +// YearViewDisplayTests.swift +// Tests iOS +// +// Year View display tests: donut chart, bar chart. +// TC-035, TC-036 +// + +import XCTest + +final class YearViewDisplayTests: BaseUITestCase { + override var seedFixture: String? { "week_of_moods" } + override var bypassSubscription: Bool { true } + + /// TC-035: Year View shows donut chart with mood distribution. + func testYearView_DonutChartVisible() { + let tabBar = TabBarScreen(app: app) + tabBar.tapYear() + + // Wait for Year tab to be selected and content to load + XCTAssertTrue(tabBar.yearTab.isSelected, "Year tab should be selected") + + captureScreenshot(name: "year_view_loaded") + + // The donut chart is inside the stats section of the first YearCard. + // Try finding by accessibility identifier first, then fall back to presence of "days" text. + let donutChart = app.element(UITestID.Year.donutChart) + let found = donutChart.waitForExistence(timeout: 8) || + app.swipeUntilExists(donutChart, direction: .up, maxSwipes: 3, timeoutPerTry: 1.0) + + captureScreenshot(name: "year_donut_chart") + + XCTAssertTrue(found, "Donut chart should be visible in Year View") + } + + /// TC-036: Year View shows bar chart with mood percentages. + func testYearView_BarChartVisible() { + let tabBar = TabBarScreen(app: app) + tabBar.tapYear() + + XCTAssertTrue(tabBar.yearTab.isSelected, "Year tab should be selected") + + let barChart = app.element(UITestID.Year.barChart) + let found = barChart.waitForExistence(timeout: 8) || + app.swipeUntilExists(barChart, direction: .up, maxSwipes: 3, timeoutPerTry: 1.0) + + captureScreenshot(name: "year_bar_chart") + + XCTAssertTrue(found, "Bar chart should be visible in Year View") + } +} diff --git a/docs/Feels_QA_Test_Plan.xlsx b/docs/Feels_QA_Test_Plan.xlsx index bd5898d..40abd28 100644 Binary files a/docs/Feels_QA_Test_Plan.xlsx and b/docs/Feels_QA_Test_Plan.xlsx differ diff --git a/uiTestPrompt.md b/uiTestPrompt.md index edb6773..875075b 100644 --- a/uiTestPrompt.md +++ b/uiTestPrompt.md @@ -1,53 +1,71 @@ -# UI Test Prompt Template +# UI Test Prompt Template (QA Plan Driven) -Copy/paste this prompt into Codex or Claude and replace the placeholders. +Copy/paste this into Claude (or Codex), then replace placeholders. ```md -Create an iOS UI test for this behavior: +Task: +Create 3 solid iOS UI tests from the QA plan that compile and run reliably using the existing test architecture. - +Project: +- Root: /Users/treyt/Desktop/code/Feels +- QA source: /Users/treyt/Desktop/code/Feels/docs/Feels_QA_Test_Plan.xlsx -Repository context: -- Project root: /Users/treyt/Desktop/code/Feels -- Follow these files strictly: - - /Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md - - /Users/treyt/Desktop/code/Feels/AGENTS.md - - /Users/treyt/Desktop/code/Feels/Tests iOS/README.md - - /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift - - /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift - - /Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift +Optional explicit test IDs/names from me (if provided, prioritize these): + -Implementation requirements: -1. Use the established pattern: +Mandatory references (read before coding): +- /Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md +- /Users/treyt/Desktop/code/Feels/AGENTS.md +- /Users/treyt/Desktop/code/Feels/Tests iOS/README.md +- /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift +- /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift +- /Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift +- Existing suites in /Users/treyt/Desktop/code/Feels/Tests iOS/ (use them for style/patterns) + +Selection rules: +1. If I pasted specific QA IDs/names, use those first. +2. If fewer than 3 are provided, choose remaining from the spreadsheet. +3. Pick the easiest automatable tests (low setup complexity, deterministic UI state, no external dependencies). +4. Skip cases likely to be flaky or blocked (network dependency, unstable animation-only behavior, uncertain app hooks). +5. Briefly justify why each selected test is โ€œeasy + stableโ€. + +Implementation rules (do not reinvent): +1. Reuse existing architecture only: - `BaseUITestCase` - - `UITestID` / accessibility identifier selectors first - - screen objects in `Tests iOS/Screens/` - - wait helpers (`tapWhenReady`, `waitForExistence`, `waitForDisappearance`) -2. Do NOT use `sleep(...)`. -3. Do NOT rely on localized/raw text selectors as primary selectors. -4. If needed, add missing accessibility IDs in app code and wire them into tests. -5. Keep the test deterministic using fixture + launch flags from `BaseUITestCase`. -6. Add screenshots at meaningful checkpoints for triage. + - `UITestID` and accessibility identifiers + - screen objects under `Tests iOS/Screens/` + - wait helpers (`tapWhenReady`, `waitForExistence`, `waitForDisappearance`, etc.) +2. No `sleep(...)`. +3. No raw/localized text selectors as primary locators. +4. Add missing accessibility IDs only when required, then wire them through current helper patterns. +5. Keep tests deterministic with fixtures and launch flags from `BaseUITestCase`. +6. Follow existing naming/style conventions from current passing tests. +7. Add screenshots at meaningful checkpoints for triage. -Test setup choices: -- Suggested suite file: `Tests iOS/Tests.swift` -- Suggested test method: `test_()` -- Fixture to use: `` -- Launch overrides if needed: - - `skipOnboarding = ` - - `bypassSubscription = ` - - `expireTrial = ` +Flake-resistance checklist (must satisfy): +- Each test has deterministic starting state (fixture + launch args). +- No arbitrary timing waits. +- Assertions target stable identifiers. +- Test does not depend on current date text formatting unless already stabilized by existing helpers. +- If a new selector is needed, add app-side accessibility identifier first. -Validation requirements: -1. Run targeted suite: - - `xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS/" test` -2. Report pass/fail summary. -3. If failures occur, fix and rerun until green. +Deliverable shape: +- Prefer one suite file with 3 test methods, unless existing suite placement is clearly better. +- Method naming: `test_()`. +- Keep helper logic in screen objects/helpers instead of duplicating in test body. + +Validation gates (required before done): +1. Run only the 3 new tests (not full suite), e.g.: + - `xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS//testA" -only-testing:"Tests iOS//testB" -only-testing:"Tests iOS//testC" test` +2. If failures occur, fix and rerun until green. +3. Run the same targeted command a second time to check flakiness. +4. Only mark complete if both runs pass. Output format: -1. Files changed -2. Why each change was needed -3. Test run summary -4. Any follow-up risks/gaps +1. Selected QA test IDs/names with short reason for selection. +2. Files changed. +3. Key architecture decisions (how existing patterns were reused). +4. Exact test command(s) executed. +5. Run results for pass #1 and pass #2. +6. Any residual risk/gaps. ``` -