From 5895b387beebbeead31d9bfe9aab7db51a2859f7 Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 20 Feb 2026 09:17:52 -0600 Subject: [PATCH] Refactor ZStack layouts to .background(), add Year View accessibility IDs, triage QA test plan Replace ZStack-with-gradient patterns with idiomatic .background() modifier across onboarding, customize, and settings views. Add accessibility identifiers to Year View charts for UI test automation. Mark 67 impossible-to-automate tests RED in QA plan and scaffold initial Year View and Settings onboarding tests. Co-Authored-By: Claude Opus 4.6 --- Feels.xcodeproj/project.pbxproj | 270 +-- Shared/AccessibilityIdentifiers.swift | 5 + Shared/Onboarding/views/OnboardingDay.swift | 20 +- Shared/Onboarding/views/OnboardingStyle.swift | 22 +- .../views/OnboardingSubscription.swift | 20 +- Shared/Onboarding/views/OnboardingTitle.swift | 82 +- .../Onboarding/views/OnboardingWrapup.swift | 99 +- Shared/Views/AddMoodHeaderView.swift | 3 +- .../Views/CustomizeView/CustomizeView.swift | 20 +- .../SubViews/AppThemePickerView.swift | 29 +- .../SubViews/DayFilterPickerView.swift | 55 +- .../SubViews/IconPickerView.swift | 56 +- .../SubViews/PersonalityPackPickerView.swift | 96 +- .../SubViews/ThemePickerView.swift | 20 +- .../SubViews/VotingLayoutPickerView.swift | 81 +- Shared/Views/SettingsView/SettingsView.swift | 1757 ++++++++--------- Shared/Views/YearView/YearView.swift | 5 + Tests iOS/Helpers/WaitHelpers.swift | 8 + Tests iOS/SettingsOnboardingTests.swift | 48 + Tests iOS/YearViewDisplayTests.swift | 51 + docs/Feels_QA_Test_Plan.xlsx | Bin 24513 -> 24986 bytes uiTestPrompt.md | 100 +- 22 files changed, 1469 insertions(+), 1378 deletions(-) create mode 100644 Tests iOS/SettingsOnboardingTests.swift create mode 100644 Tests iOS/YearViewDisplayTests.swift 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 bd5898d7e46f760aea668a724f36a720267e7064..40abd28219dfc3aec3fcc99264b135b42c7ca8c8 100644 GIT binary patch delta 19511 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 G#(x2nc$@|R delta 19048 zcmYgXQ(z!ntZuiqZQJhFwr$(yPHo$^ZQI(qTmD;YZR7Sn-TN?)lP~#_$s{>BS;L@% z{h;tlvS8roARr*nAnT-w@Pjbxq-uIF!A0y(iL10gsnN+}k(Uaxo>EV!nFrT;3ki<( z(}nz*gXSXCbw;M(t{>R%jDi{D8yZAVG9Ee2bx%CDxWhIbF{Dm{MBeDGKZW15) z7n~>L7VkA*@JVk3LTO!jTni%<4N_Y?+my%(>jjd8!hki9U9Djy3{?#2+PwVS@vGBvR z!+Unex}f_1G^pui>gA9jLxMDafpS8k{x2g5MOg4ok{}=g!=ND8Aj!-|7|CCT5Xr{S zn83v)w65QJbDOSh=a%PFG#F_#=_>I6sL*7VA;8uQixj1uY7~1FyPn%dZnk<>pp;5Q z)GuK1vSw1PhU2$IVSTd|ZLfU^baqf{=(a{j@#K4?}}5rXY3lHzv+qFqHa=P zeN0c^j50?3#_Io)>F=HdzBqoSNas{#1ANqo-TEnly&BSw&s)*g;t$dBHy>>yhI;Rx zX98WC?N#rtK=QC6R%@DjkE%6_rFBB}tRs^_X#YEZP69apt!52j^lCpC{Z+Q&Tw{{Km zdZAZs&f>hiCTO|X zftIs^M(bXeba3L{1MqZhHHrSOEX#P|xcOj#cPL|p|K8fMoTILG>@$ZAO%vz<;OE;z z5jg+z@6oA=9*=1~u=lIx_sGN^57`~UO9S2#ONW1oRn)<@URcuXzU*Dr5x9G7*M>E8J!_iM)P{`lX%h=vaTwC~aP5T}f% z8DQigf7;=BJm@R^gGuVx12p_EH84N%-$9ASi8rr+lP`R~s7K+F)?I?V=>*7Yz`QLk z?-%`#3{Avz0*lHB!sRZkik6xD1{#b*h;yPtPAzk9#E#G}N^L^1f4mWz+Eay^Tx<=6 z>AC}4I+QIUpDPy(P`X2bn%W;32|w=lcNp&1N}Ah#6n08IXKQwW*Vi#h1@ zI^Tn+>?7L@vO{WlQazy>;&Cifemm_b)zPle1KjvDbA@48;5+MmXZj!mRWH1Q_1g4a z8s9IRHm4F*@9TKcp4Zo;g|S`~hu9T7<%dci27}r)&{$Rl`JaODzYrb%@tN=7Twa_o z+uwVftOdTl|Ffuyu{Qy@p;Kb#cTpiB+6W!v;a2QVY zV4OJAG3|3D3KJ7Zg*XG#bJ0Knn6(i3mH6*PTkVw)s={x%25NBhK&Bx#kJ7=fkTzym z_sHbpZRFz35Q=12)~H}rPH1Rfa|p1Tk60dHaT?q`1(bVal@uK zA(AymYU?1jI)n!3@afQ}$VBz;NtFBy+kLTz^mPF3E|%G7=7E|(RJBk*oS(<*`r;7F z&*xf58o8V_Mfw}tn_!8z1#?TuaZAa1JBEMz#|8EF&yvSq_e3~C*4Njw!?(8ZTo-~A zZ+r@T;QkM;^RQS&VauXAXsi*WryyMwmcvT@;~PkA9e9EN{e6V5Uk)Zk&S8+9DDhuQ z$4U$&`+x&RDo09#fF^`}a3xKSf;uxHd2XT3Lvo?cQo;YYLWYxd&!11h;nFGw%4{w* z)VDLO(8$&oxLVH2R*YTjvN79!7tdEqY;wwC0QR%_^^WR+7M_SEnuNZqsaPX+qCW%I zTf)}W!qzdMSlV;IS=w7LObA9!-joMOkYLiw(|HK(*jWZaXKNe$Erk)m((#NXPJUZf zTJ;|@ul5$FcG{LRy*;Fva~dF+KB{s*tap|fG&IE?fotrWo9g$c1ag?mpF0S{Xa_yp z13BY0f!hooZivk%8F0KH^6gHZ-ojmZr@B}C+dihvxxGgAH@6u<>S;s#7M1}eTn#%n zsZVBtbkBkG^1jEy)6lum3}@>KgDXW2A}D(M6mX2~1<;J`9T<8JEdF;tH=%QwU2T7M zrlXz+I2NO(;{8!J{c-ZQ5mL+s%5$;#5@^nV^=HO-HJ)01P?h6ok*B`nB$?iV4k9i4 zbD$yDIhy9Q#M$mkl_u5~wor@$yT?MiJ&*{s3^{k2aBOC~P^GyM*v^C9N`@D}{og4b znHy(-Ud{|l0B#joN@rZcAuqTvijc$2mVH`!HJER19!%4Cey>d8;Q||03TQ@)GSItP zA}BTX{K?4x3fW=o&sXBbT_mYf6CQha!s?#}=Dz|P-5xj}#&JEI9lgRmrYp)F!ulU_ zQ%o>1^}#H|XJAcqb;VytQ8wZC=e&%zl*Do5YFEY+@W1|QbfF&2?&wsC4Ik$R=-zd0 zzvLxLo|v9^{u~C;OOlDF)S(2`Mgygnq5u8bF!y2blG}D|hxez3`L=;lKWBu65^sn& z1t&Pc9RAyeWQUtG_#E z)la6_OExY+L9~8%{Bj}NnD;`vLnLjweq!n$@(pYM5#GVd-}gBFmP-;G0y0bN8LhxF zdLxBG_u=D~S%^jP#=GI;%-TnOG-TYhKT|FEtavGU!hEW0e`+Jg`3s#`UbG{gqld)y z3Z@`@lGt8jfr}$|jUJ!*n!7^fNc+cmp5|Mv=-C(F6BZ_0UbFleJwyMn)<%x9_|j#6 z(~L6i&ffiXN+LG{)e1j!4Wuu!cX-!ZtJ!pXyw6^g@0FvMeaQ$ua$#l-l_XGexY~UCb%@nsp)P{>BidT4UJ&;J6+BS}V^@O+sF}ECfAY95xKu?M7Dd$fo!>Leq zv5OEb^c(-EWo->Iq(Sr=^~U)9{%Y_i{g1gu=A{@_B~RgeHjQ}*42vt>>Ts{pb|kR?jc|!iYN;xT%{e9dxsbrq zi&(!mr`;v{dz&XsAh;_e=GWE!b9?VI6aFSxn0iA*_ABvRydm{)LqE>i&CUGPk5#3*ureQ*jugl15x5i=dBWAjj zYQ)&7lUfCk@RFFs7K&bR`Cy*09`5F2sIDZxU%)JTKb(q zzp?ZytL&1cHmdLfjH~c6sB1g6$YjK7IKQeI2i8GH$J<5IT=)bWwiL{3fZ4KLxJcVnioAI|6MJpJV7bWPVO~tGob znL;EC3cmVO){`EL$1IK&d`IgtD2b!V4dE0j~{BKzEfxpZjC#xywnW)7BKbT#Os0}Ef z0o_;_x@_tw1cxTm{}mKIF7ow&Np%E=@9uex;cf1ODrF2h7fB1N z*8=3{CAd~j<7#H$uhpN-kXKVy_Uqk!GdmC3j|#|cMXhksheFlJkj(~$)PFpIf4`&I z(>ZAM7e)%z>9$8`7a3V5PSZ~DMp<`;>=Q;CyK$x_l@DzzK=8`luj|YS(S zAI;ku=9@yl;fEa-#dA`q(%ivz8IvZu`DhNQ$_OxydAn)dgR>^ND?jvGaH*V#!xOLA^ zyUrlnm)2JOe8WUYeO`erT7fOTn~BExa>nEPY9O+#x6T*}oI!6IxwaKfz}BKugD{4Q zB$YDI++nnO)vntKrj+|EapI~k6Jm@jq-X)50*m+v0<3Hl-f z52JiyKNt(R^!A00V4>(Wsi9$oo-E)>KMyWwNwc*JQ~q0}JOQjTXQ1sGS_?K)Ww`C( ztob&bBpz+-xr#%0w<=n?(r{ueMb?vXTRi$9jI-x({T#E0L7?O_6oTgv)3$|05fRN? zoCcDDg9<>D0=3;hT4b9EBJ6~bK<-8Fk$3UpGo-rZm!lFlEdps597fE3Tl|R};5?oC zi4M<4!(qFtOATCfb8Xfc_39sLC%J*z(EXP?Q-|=J;;Dk4j8@^WkC%%U3}Hhxxz~3a zfzccQ4a;CH)RjZ}Cu+70^$!KMa>wOLq)?q;FYF#{+Hm$MH=WmE!=1dz8cB3+E0vGh zG}%=&WYf_+aB`zEHKxeD;@WG+2+zg7DO9!yNgqoI4GUmn>QIdI#4V*D?f)bH#6F}s zR43-p;GTgn2RT@C%Uoa%qjRnauI(OJZ_bAxvezbf@!l`eARluGJGo zk9)j~0|8`%*(IcYu>`Jh%!Mw?eNo^iBmJDxmiZ_PmO5)%CA)&T5Y^G1)*^meum)4) zo#m~)r~3Yh%j z)Lnv#hjBx-j6`cJ=WsrjbsfyZ0tkR#5YbrM!_#12ZI(k1#w(D&SZpofB`8ub(FI6w zUY8u*)e738Mq$SGM{S>fmW8_cU?JhY+7bV@1WOY@1K)a|i$$T8dYT9Wts@NdqMkfqhl2Xedq^JHfH0HHRI+Z*z zw42sqz9wmf{8gfnE?)YNJ(7U23UuXukgaKtYM3N}>1J0i!LFh~LD z7w(D(FwSsKP=k!s8Jo*e_I=OEM}6CkYZe#~v-nWyixejvjnyk^v_Zol^8KQrrq z2})G|4w(7ths9{S?xZc6IgQ((mqvUdRz`f*AnTsYH_S*u?Y2>xLyU3%X=_ngh!o0k zU(!%RW_>9vx&;xzA-mu65v@^aJgkgh9%nP*q83zuH-K8&JRS7p*DwgkojS#VrZUOi zf`fCkWk3icIMl?JftJ!02tjk%{EUeXLZ()g6s!C5D(iR}K>U!`6Jl}QGQ6g^AfJOJ z!gnwGa?G;AFrL<{W4u!eg1B}F>S45Lg=7v{9IF72K7MV7T~#=vgrFTJgX}i^o8j!* z28CRA4&#Z3VwOAQ2rCN!H%BTBQfbeYP5f6y1i@>qoLc$CTCT}X(MWvB>hoJ47xFzj zXjmnDwRF*k(&6n_$3+k9?t|LGg1TsC9d46bD)BQX1GBN5v1A=G`2(j`mD=GdpG!~= z%Ge7tL1A;@;TRuQi~8g5!NYz{&gEAp;#fD5`1>8VHY=ZK$t*=+t7E}TMDYu6H{2lL z#x$e6yes1-yB;Z@<6hQ~8r~_fa2i*i7fM3B4!-xeci&^HF`1@RYfq*vYA-=b`iYsw>)DSRg;!7JsCq5_TctQ zI9d@0Qv||xmU9e?^1rn>(QI$48Flv%HB(^jS6Kmv3shVO3rt)GonTe*Ee$bme8r-m zX^tCX)%M754m}7A94Xe7B(kzFL9900x2!gy0abkSw}6L7qMr5v61ey)s|SDt#VO7U zs0i+U{eeTYc3!`7f5pumJ#kd`3aYIXFBcG$Wfz$`Z6?K}wIn%D)^zzMndDm>*Le>z?Y`|>_NhPK>UCVAkCC^8LjJw;@Q&QKqjI9Kw4{=y4+~`;%qoT4H z3JM^$0b$H2wEXEbBJ~w_lKj~pBuolxH=;cP1Cghd?%OWH_4}1!tco0F-n7C>VR>Ad z6jD3*>??WI2t5sUc`U+U#BnJ#H^bTPof7Uj(R%7@G?maYtwt)1QK z)p>BkMv&ZBy+7r7TojttRIaL7d8Rk@N9TquukpB>HsrrGMAO<(zk5Hp+qDdZYW6xf z{1gapuSTSQi{6y7%vcEm1TIRL=wV$hkYVPh?3rE(;{VFw%HUUt6Cd1%gQCb84BD%uy%CwYP+beM>ehmqMuon0Z>sT7N5JeR4KMw@scu?0t?U8hR}|wCWGsVh zfCd=rw2>xilY0{J+O6oe3Xn%NC1Rly0dYjBUgLQr5(2{7WP&EL*rVfBNidHO-7et8 zVH>!UGpiBiZQ#OGPTNJSPTRMXu?x!nIMXik5bE?Ey4vRi8xSSJRgQ%| zn@@oiMB(NHqcml>JJ#fP3S?=XsNlI7F40pNNm&Xp)lP|}DE_LC*SC!csQ=1T0jX_} z80yhura?LTB2kscJzs^telZsiYuX`))?avJN<~E@b~*Jowrvxpsdc`fn-#&DS_iqa z&@J4iK707xz|yaZiF{4GGV_k=_w(p@ab;uEt-yk`XBU3gFoc|Cf9^dn;ssyy__lm4vT31qgx4> zBED*((#pqcYJq`nssfBXEXG~FJD9w;Fh001EV8RCIYZTjfCTFYo5??-?>3XEy#mg_ zVRCIgy|0xQE1O<(H8cw@E1Q0(0%!5DYc@-k1ltja0{dX%t#r%o(aRwv%8rWi!FS`v&0p25|VHjaEvR+QNyi zM}w(Hs`?*@W|VSK1pP>cHLFd*&<}YjTIjcQ#_Z6G{kXE?q?1w>N`?Q%W`bs@)ORQ1 zcN^pWqMIRbu_bk8qD;?12Pq`Lj0Aw0IZS%iinGv+0kBX<`GDR=OJ~6~x_uWWdn&F} zUc1IFv=NfYRcSQaE8x;Sz(hl{8rx`fXY48Q>wNA-_Qu}<8=;>q=_MCs`s!QcNtgI; z1@-p?cM#Jz1EvYmsx_+Mdpb*RaVSOR-({=z?`_1jnMC#idXwd+i?IIie6>Shf%K=5 z*2hYX$@;1b;hx}u!cgIf_`=-d&UEZWmvI*!b)lY=Yp zkCgOu!!k`gqdRa4VOWnZcHfQw%k0ib_;6S<=HnUK_E=GP8_GakFyFNF96JXLKg zVq41s2wH!~b9o!wb^LlBFp(@Fbkb#&q1V@fmT9n$(XIMut@hdDeNoh)BDu`;y4HZ_ ziY6rbG&&zAa?9Vol4*~_J20Y0gECFy{?<<^<&;Rf-~{uxF!eKN*MGLxa}W@au{w(B zOvNKV$lYP-_5Fff+NvdU#kiH^AtW)w1Tdn3u{>e=)xzcPDT|7|qD(2)`${u-I(sg6 zwxOHL>yIPzxl&?&2`f>y81pRSnI-t|o~lr#lnhP;g2n$`hO{FHa5^-`xW~^w+9~Yv zMx(OI*jM-Py6mzxqLYh`QyiOW58uw6x=G5UlY_d)pQX3)KTse0RTDIC_uB!cJf#L$ z@U`;af4QrLi$$PgNz;yGnU?tuEB5cOs`OnYaHpKWnKo>gPs08L11w;egrh-COV7hn z8arIi3bQPP1x*V?j-G|`qYjo4ha_nZl&~L^I zbe9o@3hQZiF*^DOHj~bZF@kar0BIZdb{zsZk#s2bhB1}%*0BfYql}mfNW- zZ0-8gJPWyteypX^7`$5(`eZEAw4QIL4q(7eRdj=8Q^+Uyeze3#9ZcA$ZixNLnl=3D zG|S`?rYXx17@6cHMi?=`=V=80@-Y0o3h0tMTzB&=qic5nTN26gYr@a`^IJWaxTpJS zar~_TjuTcEb#UV%!A2tk80}OvX>BCh-R~FKH5bieC!Z3dR34*`B!%vBVu;XjW|&|w z(Yoc6S=&$;7f+zs0JRp*=Rxn$sRujYvtOkUo|W7#ZX=-4I*h83!$xb1chh{)!H{_I zwhW0RGF&qMl^yw!M-?JH+%ISLX+Ctz6twqHNZqXeuBbcn(MlW-#8KoY%vsLEG&$Xt z^dj%YU_9a-gFHSmT2=FdSF=>E*6MfbeW{jCQ10iN^E6Uxsw z{7GhEiXz|1G&UT7{8Bga#nf1!@^}4sHNAKcrJ^QY4*bKoo=-C9s4yWEPHMHMBz zDh2dZecl3d;YeDW5^|q^jI7tirP@ox2{!JGu?+v`HrgaWdsp=DH3>wSoT)L&?-L^+ zLOAPudQT&|8}71LIZnZ;d`>LQEP6T;N!dJPZoDPBk~QP98|he5$!cX`fMXDX((Mce zbM7cP3_g3pY(0cx_`A4j7g6C^?+L%X?Sf3rEg z*K8M%ozXHQvKz#5h^hM_9bWobdtkt??%T8dF~})_>l1ZsC?p+IlND|(_BMlXhMb^J zn0oL$7ma7_(ccm!`r*Z*gbzXV{n7gq$+Vv?wCoUTQ1^qrqLbRx99rrVEjxc6xV6^b zl)TUsVdr@hFwY@i=bL(M!LdQycK7ih9ZA`|(9#Ccz|E~>+4Ji5@c%43yDI0a7OUExKP4C?}6_uBZ6 zi_L_9!~V}dyT{j!OReXhOXBkiP5bXoVIrnGy{fNrcV9vjjSlT6sT9M&C4$Le%DEFUry=HQ5QA;{IqfB|2)avhv*4W&6wgtr+|Y@!5KJJ0d$$xT6>X(J z9je<ga%p4D^>sC)D=8~E+x2PVt;CN-FbR?l)@U*2lH3!Q=Byz^{NYJ;AyY`yQ z8Q$$wb>_be(vm3OYV&Mk?^aD2wYbbK#3*g4=(Xvfry4ME+lIP=b3AN|*nchBxHpjR zWW!Yr!S=lVc8g(wWrp`S3NWe`Xg~(CG6>G}s?{B6lQL<6RB`Xwa8}$0%b_)uwNtqgy1r!h@Pb?ir3K1M`^7#a)d|7wEpFH^)aiPpWaM)pw%bR zN7t)xSl_10-0n5m&=*#cXw`C_3_3-08Ly+l#q-<@9p3jRwsH{JxT@J9p2gX`A!L{% z61n1=&!o#o?x9mOS>esX9JA-$2zEJ3H{4mc799RzgN2V(fwyxH$Q;SnYwf5ZrwCu9 z_4E9_9yBM@wjdC9zqk{NyFt6hG0gHAx8tGDz?^7h*}9lK{N;G{B9hgT94w*C(Y2Z` z=s&_tQbwQWFQrcAb_p}go25Fy5ptX==Vj0sQcg?_rZAy#{6mjRxhiNA!ZG*Y2T#vG z$gW}ejHW9PE+U6JV2Vj^;rpcyj*){@IeLS}ks@O1;aP?+6pt(Vc^59D+Rm2iIEfZMKbkj?}r}sc@d2+vyKWf@zuxT$wAGvimwM z8O3k2C`xXIhXt}{PGZ8U>A>LV5_{Mp8{*OkBipF;cg=xaWQ+Xb4m()bhCvk-N5w}5 z_fPfrsqppg+vt_%xa02vdc%Vv*1Jzhp8l07Q>fEA6+=$BInocS%r+CkFkf=9tuAE4y6>oB;=y17 zA6+Y(St9ToxTAfMYYpXTN;P;p@5BicsBZ?>ms^qdcW*=u?o0+*}q zzKr#|t%sfE%;QX_T=2igJ8FRyP9W+9vwil*Fm`?l&T1QUwJ;6US`9qJlK3|xf`V7)gRPgk zbjW-_I+8W(1DSk{D0ir)XqKG$5@0r!MET7JQ+v-aCb<+RUaZFA@6OXjg$W>1{Bgu< zR?N9iu}DqQM^ihc3SWjT_wDJK1k*}omI^%U7XveqC`W9()7a3#Bn@x7fYA-7{!vIP zw7kDoN+nUW3Bf$gDK4ftZ8NoMnm7u$`7+F{Hf)A=qjn91a+7#^!|g;PV8RIhhhl1c zrn7RgQ3j;Zy=S8W|7!FnZEwJF59ub&h0)gLzmt?l9nC`yeq$tSxI+DzotImd@!+ND zaqMPuGjJ=f@`g53c+VeIn5lyoe+0S?m%~jmUzO>F77=i*2^cH?ZrQh{ZvsJYqNR)k99ljbc;|wqE0GGn zJ)>4bAULWkS|MjHVm^gR8&nX(RT!|?u2P!j!^|f9$Yy>Xpeo4jP}m#X67Z%a=!@8D6Q-LZpr|(oK*9v6qi&8>tvJP-*x# z(%pzk3XTa65&R2aFo~rQCYR<+@ z2xLib`7y`bn6Hq4{FN+{--d%}TTE|)zuG0A2tz>A?0$b=TxlIl#a|K~4QW#r(0#bt zQnSf>MW-X33&;63f!z|y&=PqLVRf{4l;&0>&wN9+`8fs19E#-Vj&7mH#(BA_X4sSk zXsN|6PZhRJDy0$ys1|a+I#Mpm)`$j%FR#Q6#$1+E5@u4`zdI3 zBz&0W7Bt0R1EHRd`7orXBaf1V8531drIXl*z}JtjgS>3bsZeFziJ+Id>iR?>7~Sg+ z4-bTlF+fL3_%6(dPB8onNzh$}Eo5YXq`*(AJjxrb<4M2IC>UUuHEfIDSFR&%$?Zav zAL@8ipWmC#tQO-p+P659chUa$6HEBls)z3t4!FDyG%CVnL8fGa(gl>|N2>9d7k($k z%z?@LPpr9-HWtHHCBc7lyiI>PUm*HmrP#w;+h=Gv(4jb)sV;UQ6iK&yBGD4tqk6(KYb0K`FGl|JP*MAj2hdk_58HZP7}$yyNh@O)F3G+R)@cKoo}8^ z`gx^r#-Q#$6D-pMpawG9!7%MT)Kxkgyq;I$7goLMeONy=bHr5wbpP;Ajd30$r}9gH>U<^)~MhNN0xMKGfN(qB|{&QacXC_-%fKwjw~W6Pe<9 zcBU?NtZ2RoEncvJ7E`b}VXf)t+q%?LmKu={d#xSo{AMAoXIvSkiYig{0Z8~Z;}G;a zfzNir!{42XRw|wPl>BH8MLNIs((?6RQE#iI(`z`C@_$Y>HGf`>!}O`$;6jFn7Q1Th z)H2)Q+>34$5Zyxh3opUw7AgZ=c85ANk`29-n}-~fhW{mQHD1EOskPsRmi!4+pT*q% z*QMvt;e+!mst5+OQ+!=I**ilN&R~S_X%b8E?$ffLhE?lk(2j?vgC5d4Jgx_vCf|5n zXk}1vtN6Fn>a;SEUtk!a6=$=y5?fkeK3LyTxI4f zaY5~hF!)1&SIvbPN~UEsL}+Lpk;{^J6OREZFMm7C>o%$We;ic zfMSc;EvJ>#4O+`K?ub!r!%2HWC`_@z?RwTuLMY_ND1GOvz4MlVU4k42M9LJNjD%w& zaA_*;P>Jn5^pLH+o|Vz}ptZ$2oXP^4`FeH`H%%_pDkm_v7 z-o5`Pt1h?#H5xaG;vRI8f>{)~WZK=YJObQN?WiD4XMySFk5S(YLM>Z3M)6yVN)@^9 zO&7wvQ}c>Hk#Kgg;iU`mRkk&U`#>sftE1@rpR0dJ8E~;0o~{j>{Jjx53&|6Wh^Oer zADD^1(<5^dr~nsmX{;ge#TQ(Qm(3)JZrScJ$Ycu-PdZ`Q3kUU19d(SSngUex9r8sP z`(|)k36q0lKFQ&pu>L)JbN2l7;*um@Jjv~}2VX~@56qX}>67^ail5L)K#8YzN_}kw z`T=cHjB8q^w2OO^`yZO1nU(O)lIUuZ@UR#AJc7&iKtO@0_;HkJ&@#2AVt^xy&)19Q z>K|@xFUa_1v5Y)Z#={wTjBd^be3g?Y*r_V|5C3 z!ZQP7Z2$Qd)yKu<`S)H82ic_6f+}gjmG?{8*7x#j??=d%iHbfsi3e|1nS_)bkoJ}j zgp*C$a{#SsAUCMu^h6+PV-qrIs6oA}z`M{bmC-XxRoeBxiHtc_-!Re|% zSCl_b;tlqE_TW!n6yWnlvjS#^lC7G`FR=axvw!o&<^)9DqRUUG`PB1|E5W5<79Fhh z)3e80s4FA%s2oT33Ze6iR@EH-yM=HMI1yAC!T~OKd#jqRs8kSZ-_u23L{xU7Vu~5F zW2{mVg2x-M7*?K2N_p#<)gjkFgCH#M1O#km(5et$e%s(to#~bKg9{F0T_b+m5=yPT z$;+sbk&*1OJ7OJh6Z7G$UojVXgc-Y%Ze1gwXbO{rnzg-jon!(!IdP=R`BM_~NI@72 z3CR6?CkjvgR~f)?s|{jzpwH2?AM=AP3szrW%EvKlXZu)L=YYkOU=+r@H>#4|_u^v6 z^bZ|;*_E`Jw&XBK?Wn+m`D5jLvtm#QID6!DKwn>zU$13uNp^ijT$t;Ma0A0T*#Ur|sd zVj4Q4^<+hqYImtGtsgvq&Kqu!pY&mK(8}PmBrS5qaS40hvz@DOg)?D$Bk7+hczXAq zNK8XA&u}3%u^_D?k}HL`Pqkj+4ivZ?wF*o}DpI$LWK>@=^0qCAsogc)9Ynq6=_R(_ z!Mo{zD;iRIz48r`N$uD*vo;TmF}zgKztsF;Q+DY^d|YS5cpsZ7;Byi)2ydgy#qo@N zFiScWDsMfbBlfY!Hc9$5Yg#o-4}lVk?TC!TV!kAy^)HOTP?gD}{A%*j4Zs~2Fz_!$ zo3dMf{Sugx4%|L2kVurrSqQ z8&qZ5atcdap@B!1ZK37nk(oss?OHEVf=Cz4R40GcwB$?436&S4T(0c*%_DSipd2=W z*N{|#o3Uy_&G;yuNf)~xz|KP(%fJ#BL@UFQCxFv%hG!VP5L3mU4OKDnSkc3tc3b2R zilq?pz4b;l6lf`y;EoJmr9(a=L}?7k2SX;+rF59qQYvw=TqI2yb%~|^ONLO9^l8cO zcz#ZIx3+x6vcvb_RpzQw*I*Y-g>m%|r$mQ-LdZfGk`Inl>eqx9px9_DjhvPQl|miX zMt&%80wYrc)mzS=JJ1kFx;{cng0#IA@v+X-H6t%B)*g^b*E4dfbxYHvES;;kul=@L zbWMB62^$Zx(BRo1@SM$*-_$u7$S0^@YRPSXg9nzmi+%kbM*MG}Qfa|t_EMvn(M6un zN(?<-Epx=nV?tCNfL?vt;N2;>+h|`xQ1Ld=TbZGIaeZnpWTMpf6AzWvwRB?UzCj?o zq55Iw$=#-v5I7SA?}aIr;5ML4y9sLb3T``R+7!%uktbM)XYb|hEge@6k^i8wAORtG z{rkB9BwM<+`>$_1vL)Kzdk<+uSw(*KRx@c-_Z}>y9w}rp;1EM?Hd0%{N)%kbHEcQoIYa$fUllL##pEnA7fJ6{~&IpH!>TIk8ph z{0tB2Sy1&-DOT6Cl5l)cqhhuf0yccbudNDv#jBTHPN|=+#(a;VjdY^g5dSr<;q0;b z)mee&f;gZ6G+>!hIM-rP#4m`y|BipfC|o^a#6i4J_X=wL8MREh=Id2FFp(7w=FHPp z5>7-ett1JSG%QA&2PG;Bf6mieTm(ys`Hi^;dk)*_q=I!b%gV6epJfMn;*%|Dov5Wx zm8On%oM%YYx9)B(P~GtJUqIxlw=mM1hT;fTGQ(*W0GQ^Jbc)Zwo0(V)ps@v&#Fz>1 z#F~*@nh}+fCjA__Z%MRs%L_N_jF5(#EQ26yFi@KvNUlP8wZ|6GpIe?$GFh$WQ0FdV z6x%NteS|k_vm9vV|CS;tEZ{SRm^NIqeA`Sg8S*3Lg+%}6^SgGmRF%gp>(290ji%|< z>;wZ|;hd9h{N6Go-+p}1ayd(nO|fcG-28Iv_Vae;2UDz*#10sv!dI;Jwd7c1>3mx* zfd5q$dx7Na96+X?bFdGa|CSBcIE;D0mSUX`s`v(-L}~)@lF7l@6in z8HR+UDNN>5> zrWz3TUlnAi?3v!urW!=;3cmj-4POMQ@Ao-JCH2(`$B{4N{7fi!Ix}|ZM-xlzIKeyM zyA8%ykEr&m1xmAtJ$P0YroFLHQ)^)%nYjruO4ux#8FwmVF@Y)ud=fFjjSd<4ALg&! zI=w}RC{kcV7{%3C;?XWc|5ZyI5<3S%urmJ&xP%a7bj3&&{HzU!nCEovqx%tOP?xa! zB$7oHZ$^L_+=3a;p5k3XGEWARM34X+X($~oh$DtWW$WfmWe;wI&T(j^#e{2x=Qx@# zTt*8>CtWsQaxclLa^0Xo;+cztpYV=cC>(!`()wm-!pUBfoO>^OvnYtp2jCeiz5eW7 zlFTZWroS&QxI69=r$I26v7S(s2{MD4kl|Te4acy6XgP;rYlO1yslF8Gs^J8#Do84< zIme0#ThSn%s#F78n`)OQ!r=uv-92ygHh;IbYkfhQ9@Dg~s~p;#h>Ifd7h8l3S{0;S zTl)xD=b~(yWQMU$v;Wnm6m9|&2|c*iqMzQ;tNjcF|lIu}xRRkB)4dCA~iz&&gmjt>C21s%K?@MjR8 z$O!276}ZX`CJbXpv!ZSm6V0PQC3jR)fJExRxGhMLB8~YvzM&2YGlN-4o|LVUn;up& zi@?-I%r6A+ojlxYxwgspr?nKpKg4#jTL;3f?KWx0M~cT8_N@jJe z63|)bnfd-Y8&As-Lu*db#_U3F!dLf!3l_=__a_sn*3f@a2EA6D4kS`eLDgqhXU`Y= z_gGx;z&XD=tuwR_KD#tC%`h9WMu30hM<7V*Xwc4wworwe5da`y9hji(DKG^jiXbpa zq3-EkE>OFpIjCw)N6sxgCaEL4(>eft;Te<&|Zd z8&0@Exb^ukEI=9p`v7@gogz|Bau;oDel!O%#m_NbNJs|;N(*u}7P7oZ6ms#7&QU`$ zON-%b@~&5(d`qYrPHYUr;Gea$5f`E7;IUdZa>_n@H2{5Gb9<&6--mI-ETZ?~b(p$q zh$o@LkLZxXoo5I1!G4OvFT^jbiW*5b%iMJH(kHq0_C%^h69R~D-WFMtBGXV5TfURt z-k$?DfsMa%NpMWQ7sG0ABV(fSW?H8s6*}$#<%f^j7ZacITln#;f$3Et!Q|1aE3>9b z+k9EX0CQTUp~mU__BLN9=9v+8Lu*G`;!JCdSDko54+B~FKXK%~wvp9Ya^W5t4S0xJ zgs~x`S3O$>En(wz5i|*~M7*xZImz%Xgq-%hGFg}>jyf2(T4qJK$C8RXSRM68*}n#y z7`}CV7s>SP-s$vo7nVDi5`82BtAj0!17sv4K>3)gE2xBJAX01oKspl6-~rT875v(N ztHIX?LrhUqASZpy@jCQ#QWnkf5uuQ?a9WOd}jv+$3=QpfM_#RbOJ;*JsM{}iK zj5H2&8PK1ri9ZBii%$n4R}11{4N&@|n)n>AcZ@$&QqPE;pCJdGI5Q|P=drkB-1V+K zfulrC#Fj)yr9^l7i}ry>jsWvlscJW~uf=}aK6;}ZPHetl*XUcE$&~qsazAis&^ZTV zSIuYq!^#T@Mk_BU(-qHDBRJoNL(ZS+%f#`7SBNP`8x_9Gxkbe`X=xzF9r1~pU9qny zn+uLa6z25I$-6R{6=cahcyXAahxdEpz)K7t4bd4hc1Y_)cB0%)jMuG10y^>$=C5d? z-S>ue)}5gD+3$Oj8mQ7i4}nX8S2_wJe9M1%`OUoR@nb6(kq#HMD*va9bB||2{p0w| zeXbiN%q4P5OKwd}<#yO9%;m6HMkCd6X=i9|%{66+nS(IPArf^=loQ$*o!r*kFC&ti zM6bE!TK%STe!pJl^?UyLKJV}IeLm0g-}BG&{h;-@$DEI=>dqPc&gz?-%JJ~PH{UA5 z@<9(CqD)@4+43`*C1^Q%ES#FG@bmphQws79*tK7R0^H@1+U{s=ague)pX5H)MNi}I z6lHJ_3-*89`=$1Djrkq(&YE;3%A0G>;ET<@`|xgF(XSNR(Su_H#E59HnKm}5)3W$e z{yEJ{{7DL(_ip3{gEOjfMa33k`7RbX26FNkLich!LWq-IwttMeq8t6fY7FHvhtQV07q zHZwbu6TQydQZKf~dZ}^*H2@nrBEK#Eb~L-vsrw2}GT9lnA8WfhR*m%d_*&1`6^K0>`B zc}u8zwJd691^JpmJRR$>kr7Y+f_5AbdyHDE1d-BX8Z2com0J$eaom?0QU$8@=2pwR z3r{*to|(TJgV`{oXp<^aMZ#0StOtmDlPmhy;=0T$*;*Es2wIORsHQJGxZm{nuZ4BSoD50I`Gmvum@`kOt~TC1SNYC4=Y^oX4U#Jg;*Qf> zS(LH(&?)j&-Vb?h1>b8!ebdmdm|f56L!+7olL@PBE^$;l+=d zf&^cb#3Es!D@Qy~I9d|r(5!VSWYvd4GJB6C)-*ZvGOST*)pYhVD-1O>FGe)z;PyTo zBNBxx>%(`9STEh|$AI$ySrvTfYVX%+E5vA`jCZD9$ z8CfYTZ?b|)ck!!&`IJLQ@m3ziB~Eg!%eNYkoWnEu!~XcqR~?$wp#Ae;9T!qJm@*}H zjXUoCC3-Q>H*68Pg|_MgR5@&A1a$@Xc$}n^3!k>NE_upq2g%W0%FssoVF<@xR0FK@ z*8(iB)SBEx&DFvCwL_f2O;xoPgoUzpMP-fd$f0m`C%>`DPTJedh5_kppSQhe-SAFC zw6;_0kH5XWpWz*6Vq-)6;!l#ie+NlfGD!h7Fl`V|O4c0Sbh5R77($QZ8JDQs%IcyT z{nvrSG5to*Kgo`H&&)ly=Bj3?Vs{Ta*uFSn2`z)A0V_7#)R&G77u_1+PSvYqMFvW* z`NVaj-}!bTUTZmlUJ<08(e=azrw!CYAu*y-ZR&E?Ik*!m*T~15e9DjA^MOHvj^yg&X!hVkqL(D?+gjtc(sYD< zMxv6XshTrh)G@4$&qy~l_* zqdT5wSkGc>T27^omXkUzpAB@u^$=8&%2h8U@oW1l5I0}W$gQfG|D{6JWfqI((VEA*O;6!V)Wi{pwL%-OXAFo69?V~;RF1V^0pc}IIK(7tD47`rLWdyC9*uo0R?|b zx2FUj5Ih*JVKWoLE5-0=v9AWvBflD zGiLRIBUyZ;={$UF)j8(+X1V(W%Z>`&oOsJN91eP;{NaL{OL7bOf|OE-*{LsIevZ(u z7c`i8nqE%U-<=EHeDh`l%dOc^8V)x*f$=(aiw>TN*{!5+iX&U!i-(J`iURl-6`PgC z!*y9j52ky~h$IESwy$aYul&;zt|@#c2~G+hCE=RNM@w+lXPQnTM$+cD`4|n_hjv0d z@uA$`NqEA%7@r4FP5BmRs!W0~F$}CECcP^1;L5j==tAdd4)lK}#rI)9JPQ>-j}r>wUP3EqZOZbRm`A$mKI z;2lWi4umwn1JT=s=&beAMPRbEvM{+?IhcH{JPiEM?Iv0A(`9ov;D`Y`6@;&_#Z=3W z9AwaPfr9j04e$&@`LjXzbnVRETk6X8Z-kgg<;zkn%jbdAG3b~+d!DK>}gAo)nAvl&XF=X@3G5FmdOLu{d1=ble@zmEZ3btHA z{SBY)MgwDgvfwus#5ea3Fh4RFJyB~M*w~e2q0JrL?yd##QQT4W`d!0NfoBZ%-xfjH zz8d2XMF4>M=We4Sz)`LGLa$jeA{-@#hM>*}0TqS+>-yx0$xnXc}4A$N3coxk#) tP>(+pXmu`k!C!5UBm{tV!3XDl~Lc^@Bke#z4Loo{{cS6Zm9qO 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. ``` -