diff --git a/.claude/knowledge/gotchas.yaml b/.claude/knowledge/gotchas.yaml index e72785a..3c81281 100644 --- a/.claude/knowledge/gotchas.yaml +++ b/.claude/knowledge/gotchas.yaml @@ -19,7 +19,7 @@ how-to-apply: Free to use iOS 26 APIs. Don't add #available(iOS 17, *) checks. Don't fall back to ObservableObject for new view models. If you ever bump the project default, bump every per-target override too — mismatches break @testable import with "minimum deployment target" errors. - id: secrets-are-gitignored - summary: Savely/Config.plist contains the OpenAI API key and must never be committed. + summary: Savely/Config.plist holds a local OpenAI API key and must never be committed. why: Leaking it on a public repo means key rotation + potential abuse charges. (GoogleService-Info.plist used to be in this list; it died with the Firebase removal, 2026-08 — the historical copy in public git history is neutralized by deleting the Firebase project, not by scrubbing.) how-to-apply: Before any commit, grep the staged diff for this path. If present, stop and tell the user. @@ -28,10 +28,10 @@ why: The Xcode UI test template ships SavelyUITestsLaunchTests with XCTApplicationLaunchMetric which runs the app launch ~8 times measuring performance, taking ~5–10 min and flaking when one iteration exceeds the std-dev threshold. The template tests assert nothing meaningful — they only measure launch time, which is noisy on shared runners. Burning that many minutes for no signal is wasteful. how-to-apply: Leave -skip-testing:SavelyUITests in .github/workflows/ci.yml's Test step until qa-tester writes real UI tests that exercise actual user paths. When real UI tests exist, drop the flag (or narrow it to skip only the template's launch-perf tests). Build step still compiles the bundle, so breaking changes to UI test code are still caught. -- id: ci-needs-placeholder-secrets - summary: CI generates a placeholder Config.plist before xcodebuild because it is a gitignored required build input. - why: Config.plist is listed as a required build input in project.pbxproj (Resources phase). On a fresh checkout (CI runners) it is absent and xcodebuild fails with "Build input file cannot be found" before compilation. The real key is not needed — CI never calls OpenAI. A placeholder with the right plist shape is sufficient. (GoogleService-Info.plist was also generated here until the Firebase removal, 2026-08.) - how-to-apply: The "Generate placeholder secrets" step in .github/workflows/ci.yml writes the file. Keep the placeholder's plist shape (OPENAI_API_KEY key) in sync with what OpenAIClient reads at runtime. +- id: config-plist-never-bundled + summary: Savely/Config.plist is NOT a Resources build input and must never become one again. + why: Anything in Resources ships inside the IPA and can be extracted, so a bundled key is a published key. 1.0 shipped it that way (key revoked 2026-08-19); 1.1 removed the build-phase entry, which also retired the CI placeholder steps in ci.yml and ci_post_clone.sh. + how-to-apply: If Xcode re-adds Config.plist to "Copy Bundle Resources" (e.g. dragging the file into the project with target membership checked), untick it. When Tips returns, serve the key from a proxy you control (docs/plans/drop-bundled-config-plist.md step 3) and relax the archive gate in ci_scripts/ci_post_clone.sh at the same time. - id: force-unwrap-ratchet-active summary: force_unwrapping is severity error in .swiftlint.yml — any new value! fails CI and the local pre-commit hook. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a015f0a..2e07948 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,24 +68,6 @@ jobs: # how to tighten this over time. run: swiftlint lint --reporter github-actions-logging - - name: Generate placeholder secrets - # Config.plist (OpenAI) is gitignored locally, but the Xcode project - # lists it as a required build input. xcodebuild fails on a fresh - # checkout if it's absent. CI never calls OpenAI, so a placeholder - # with the right shape is sufficient. (GoogleService-Info.plist died - # with the Firebase removal — see docs/plans/remove-auth-firebase.md.) - run: | - cat > Savely/Config.plist <<'PLIST' - - - - - OPENAI_API_KEY - ci-placeholder-not-a-real-key - - - PLIST - - name: Build run: | set -o pipefail diff --git a/.gitignore b/.gitignore index 8fdd2b7..1d8f269 100755 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ fastlane/Preview.html fastlane/screenshots/ fastlane/test_output/ fastlane/README.md + +# /brag launch-video renders (large, regenerable) +brag-output*/ diff --git a/CLAUDE.md b/CLAUDE.md index 55fde84..a5e857a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Savely/ ├── Resources/ # Strings.swift, Localizable.xcstrings, Color+Warm.swift, UIConstants.swift ├── Assets.xcassets/ # Images + ColorPalette ├── Savely.entitlements # Sign in with Apple -├── Config.plist # OpenAI API key — GITIGNORED +├── Config.plist # local OpenAI key — GITIGNORED, not bundled SavelyTests/ # XCTest unit tests (skeleton only today) SavelyUITests/ # XCTest UI tests (skeleton only today) @@ -92,7 +92,7 @@ These are invariants that future changes must respect. They were discovered duri 1. **Signing is automatic, team `ZHLD96SP29`.** Don't switch to manual signing. Don't edit signing build settings. 2. **No `.xcconfig` files exist** — all settings live in `project.pbxproj`. Touching pbxproj is risky; prefer Xcode UI edits and review the diff carefully. -3. **Secrets are gitignored:** `Savely/Config.plist` (OpenAI), anything matching `.env*`. Never commit these. Never paste their contents. +3. **Secrets are gitignored:** `Savely/Config.plist` (OpenAI), anything matching `.env*`. Never commit these. Never paste their contents. `Config.plist` is also never a Resources build input — a bundled key ships in the IPA (`gotchas.yaml#config-plist-never-bundled`). 4. **iOS 26.0 is the minimum deployment target** for the app target. Use modern APIs freely (`@Observable`, `NavigationStack`, Swift Testing, etc.). 5. **Only `Savely.xcscheme` is shared and tracked** in `Savely.xcodeproj/xcshareddata/xcschemes/`. Test commands must use `-scheme Savely`, never `-scheme SavelyTests`. 6. **Localization rule:** all user-facing strings go through `Resources/Strings.swift` constants and are registered in `Localizable.xcstrings`. No hardcoded literals in views. diff --git a/Savely.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index 4d4d7f8..4a29ef1 100644 --- a/Savely.xcodeproj/project.pbxproj +++ b/Savely.xcodeproj/project.pbxproj @@ -7,46 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 098D870F358FFD061FE47F1A /* ReceiptParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740B73ADB53C98937307DBAA /* ReceiptParser.swift */; }; 120AE56C41CBD54B070A8436 /* ScreenshotSeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */; }; - A11Y0AUD17BUILDFILE00000 /* AccessibilityAuditUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11Y0AUD17FILEREF0000000 /* AccessibilityAuditUITests.swift */; }; - F25F23C6EEAA47853BFA137D /* StoreScreenshotTourUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */; }; - C5DDD46B40453371BD90C01A /* ReceiptScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */; }; - 7479BDFD4C96ED1C9E05A119 /* ReceiptOCRIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */; }; - 31A69355DA4D561A928D09D7 /* DocumentCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */; }; 15C9F87752F242860A062A9C /* ReceiptScanFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A17E22922859025B4DEE1D /* ReceiptScanFlowView.swift */; }; - 6BEBD87A257D196FC2B5F572 /* ReceiptReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5F5E26408477F8E6C59BC /* ReceiptReviewView.swift */; }; - 5A923D851E1F0A9DE0182E2A /* ReceiptScanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 410C0D1F40524DF8CFE25021 /* ReceiptScanModel.swift */; }; - CEBCEA30F32B686EB71A2866 /* ReceiptOCR.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C90599A224DF05C1C7BC95 /* ReceiptOCR.swift */; }; - 46C940AC7FC071D52F7037C9 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 9FDC7BEFB3FA01CDB951DBBA /* Fixtures */; }; - EB84DBEF8B09F9EB1E7AAFF6 /* ReceiptFixtureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F50EC75E1A4FF9BE4992A /* ReceiptFixtureTests.swift */; }; - 9AB16553AE439FAB12574217 /* ReceiptParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A2405B0A3780703DFC27C9 /* ReceiptParserTests.swift */; }; - 098D870F358FFD061FE47F1A /* ReceiptParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740B73ADB53C98937307DBAA /* ReceiptParser.swift */; }; - 5522F0A81CBF724D40756A20 /* ReceiptDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */; }; - 5C26C81FFC3D8C3867044BB4 /* ReceiptAmountGrammar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */; }; - 6A21AE5F9EDE58BDDB5A1514 /* ReceiptVocabulary.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */; }; - 21A73EF62C20FDA050059206 /* OCRLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA2F094D50821E2DA0497A4 /* OCRLine.swift */; }; - AA28B8C8B38F312A7623F2D2 /* AutoMoveSuggestionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC2151C01A7FBD2947EB51 /* AutoMoveSuggestionTests.swift */; }; - DE826483AC71FF3EC2A603B7 /* AutoMoveSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F774124079B5BEC3DD37C743 /* AutoMoveSuggestion.swift */; }; - A11A0002C0DE5AFE10000002 /* AmountParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0001C0DE5AFE10000001 /* AmountParsing.swift */; }; - A11A0006C0DE5AFE10000006 /* ReminderSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0005C0DE5AFE10000005 /* ReminderSettings.swift */; }; - A11A000CC0DE5AFE1000000C /* MoneyCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000BC0DE5AFE1000000B /* MoneyCategories.swift */; }; - A11A0010C0DE5AFE10000010 /* DepositModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000FC0DE5AFE1000000F /* DepositModel.swift */; }; - A11A0012C0DE5AFE10000012 /* GoalDeposits.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0011C0DE5AFE10000011 /* GoalDeposits.swift */; }; - A11A0016C0DE5AFE10000016 /* GoalEditSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0015C0DE5AFE10000015 /* GoalEditSheet.swift */; }; - A11A0018C0DE5AFE10000018 /* GoalEditValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0017C0DE5AFE10000017 /* GoalEditValidation.swift */; }; - A11A001CC0DE5AFE1000001C /* GoalPace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001BC0DE5AFE1000001B /* GoalPace.swift */; }; - A11A0024C0DE5AFE10000024 /* KeyboardDismisser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0023C0DE5AFE10000023 /* KeyboardDismisser.swift */; }; - A11A0008C0DE5AFE10000008 /* ReminderSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0007C0DE5AFE10000007 /* ReminderSettingsTests.swift */; }; - A11A000EC0DE5AFE1000000E /* CategoryPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000DC0DE5AFE1000000D /* CategoryPersistenceTests.swift */; }; - A11A0014C0DE5AFE10000014 /* GoalDepositsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0013C0DE5AFE10000013 /* GoalDepositsTests.swift */; }; - A11A001AC0DE5AFE1000001A /* GoalEditValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0019C0DE5AFE10000019 /* GoalEditValidationTests.swift */; }; - A11A001EC0DE5AFE1000001E /* GoalPaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001DC0DE5AFE1000001D /* GoalPaceTests.swift */; }; - A11A0004C0DE5AFE10000004 /* AmountParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0003C0DE5AFE10000003 /* AmountParsingTests.swift */; }; - 510740529955B0B1E6EA730D /* AchievementEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FA742D1E24F010F3273E30 /* AchievementEngineTests.swift */; }; - F764EBD092BA4435466DD9AA /* AchievementEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12789AD27FF1A7114156F945 /* AchievementEngine.swift */; }; - ED04D2F84390584EB7EC0615 /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE2C1ED73D49DC33599EF2 /* WelcomeStepView.swift */; }; - 699FCB7F699B1E9C9C644AF3 /* SproutMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38DAD27B5231BDC080F36DA /* SproutMark.swift */; }; - 8DA6B54FCD7F5EA515DBC1CC /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693B815A380D566280140319 /* FeatureFlags.swift */; }; 1D3669872CC62F8200E84FF9 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3669862CC62F8200E84FF9 /* Strings.swift */; }; 1D3669892CC62F9100E84FF9 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3669882CC62F9100E84FF9 /* UIConstants.swift */; }; 1D36698B2CC62FD700E84FF9 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1D36698A2CC62FD700E84FF9 /* Localizable.xcstrings */; }; @@ -61,7 +24,6 @@ 1DAF5D1B2CEDC118001C0AF3 /* GoalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D1A2CEDC118001C0AF3 /* GoalsViewModel.swift */; }; 1DAF5D1E2CEE9695001C0AF3 /* TipsAndSuggestionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D1D2CEE9695001C0AF3 /* TipsAndSuggestionViewModel.swift */; }; 1DAF5D202CEE9767001C0AF3 /* TipHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D1F2CEE9767001C0AF3 /* TipHistoryView.swift */; }; - 1DAF5D242CEE9AF4001C0AF3 /* Config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1DAF5D232CEE9AF4001C0AF3 /* Config.plist */; }; 1DAF5D262CEE9C13001C0AF3 /* OpenAIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D252CEE9C13001C0AF3 /* OpenAIClient.swift */; }; 1DAF5D282CEE9DCF001C0AF3 /* OpenAIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D272CEE9DCF001C0AF3 /* OpenAIModels.swift */; }; 1DAF5D332CEEE7AA001C0AF3 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAF5D322CEEE7AA001C0AF3 /* Notifications.swift */; }; @@ -72,7 +34,6 @@ 1DC014132CE5B0C5009A46E1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC014122CE5B0C5009A46E1 /* ContentView.swift */; }; 1DC258082CC07DCC0002DDE3 /* SavelyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */; }; 1DC2580C2CC07DCF0002DDE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */; }; - A11A000AC0DE5AFE1000000A /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = A11A0009C0DE5AFE10000009 /* AppIcon.icon */; }; 1DC2580F2CC07DCF0002DDE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DC2580E2CC07DCF0002DDE3 /* Preview Assets.xcassets */; }; 1DC258192CC07DCF0002DDE3 /* SavelyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258182CC07DCF0002DDE3 /* SavelyTests.swift */; }; 1DC258232CC07DCF0002DDE3 /* SavelyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258222CC07DCF0002DDE3 /* SavelyUITests.swift */; }; @@ -97,12 +58,50 @@ 1DE9FE172D066EE20069AA64 /* ReportsPDFGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE162D066EE20069AA64 /* ReportsPDFGenerator.swift */; }; 1DEACCDE2D0BE6340084CC39 /* NetworkErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */; }; 1DF7228E2F9C182300320B38 /* Color+Warm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF7228D2F9C182300320B38 /* Color+Warm.swift */; }; - A11A0020C0DE5AFE10000020 /* Font+Warm.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001FC0DE5AFE1000001F /* Font+Warm.swift */; }; - A11A0022C0DE5AFE10000022 /* WarmTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0021C0DE5AFE10000021 /* WarmTypeTests.swift */; }; 1DF722902F9C18FE00320B38 /* GoalDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF7228F2F9C18FE00320B38 /* GoalDetailView.swift */; }; 1DF722922F9C19CE00320B38 /* MoneyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF722912F9C19CE00320B38 /* MoneyView.swift */; }; 1DF722942F9C19E600320B38 /* AchievementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF722932F9C19E600320B38 /* AchievementsView.swift */; }; 1DF722962F9C2FA400320B38 /* AddGoalFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF722952F9C2FA400320B38 /* AddGoalFlow.swift */; }; + 21A73EF62C20FDA050059206 /* OCRLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA2F094D50821E2DA0497A4 /* OCRLine.swift */; }; + 31A69355DA4D561A928D09D7 /* DocumentCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */; }; + 46C940AC7FC071D52F7037C9 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 9FDC7BEFB3FA01CDB951DBBA /* Fixtures */; }; + 510740529955B0B1E6EA730D /* AchievementEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FA742D1E24F010F3273E30 /* AchievementEngineTests.swift */; }; + 5522F0A81CBF724D40756A20 /* ReceiptDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */; }; + 5A923D851E1F0A9DE0182E2A /* ReceiptScanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 410C0D1F40524DF8CFE25021 /* ReceiptScanModel.swift */; }; + 5C26C81FFC3D8C3867044BB4 /* ReceiptAmountGrammar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */; }; + 699FCB7F699B1E9C9C644AF3 /* SproutMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38DAD27B5231BDC080F36DA /* SproutMark.swift */; }; + 6A21AE5F9EDE58BDDB5A1514 /* ReceiptVocabulary.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */; }; + 6BEBD87A257D196FC2B5F572 /* ReceiptReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5F5E26408477F8E6C59BC /* ReceiptReviewView.swift */; }; + 7479BDFD4C96ED1C9E05A119 /* ReceiptOCRIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */; }; + 8DA6B54FCD7F5EA515DBC1CC /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693B815A380D566280140319 /* FeatureFlags.swift */; }; + 9AB16553AE439FAB12574217 /* ReceiptParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A2405B0A3780703DFC27C9 /* ReceiptParserTests.swift */; }; + A11A0002C0DE5AFE10000002 /* AmountParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0001C0DE5AFE10000001 /* AmountParsing.swift */; }; + A11A0004C0DE5AFE10000004 /* AmountParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0003C0DE5AFE10000003 /* AmountParsingTests.swift */; }; + A11A0006C0DE5AFE10000006 /* ReminderSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0005C0DE5AFE10000005 /* ReminderSettings.swift */; }; + A11A0008C0DE5AFE10000008 /* ReminderSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0007C0DE5AFE10000007 /* ReminderSettingsTests.swift */; }; + A11A000AC0DE5AFE1000000A /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = A11A0009C0DE5AFE10000009 /* AppIcon.icon */; }; + A11A000CC0DE5AFE1000000C /* MoneyCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000BC0DE5AFE1000000B /* MoneyCategories.swift */; }; + A11A000EC0DE5AFE1000000E /* CategoryPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000DC0DE5AFE1000000D /* CategoryPersistenceTests.swift */; }; + A11A0010C0DE5AFE10000010 /* DepositModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A000FC0DE5AFE1000000F /* DepositModel.swift */; }; + A11A0012C0DE5AFE10000012 /* GoalDeposits.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0011C0DE5AFE10000011 /* GoalDeposits.swift */; }; + A11A0014C0DE5AFE10000014 /* GoalDepositsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0013C0DE5AFE10000013 /* GoalDepositsTests.swift */; }; + A11A0016C0DE5AFE10000016 /* GoalEditSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0015C0DE5AFE10000015 /* GoalEditSheet.swift */; }; + A11A0018C0DE5AFE10000018 /* GoalEditValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0017C0DE5AFE10000017 /* GoalEditValidation.swift */; }; + A11A001AC0DE5AFE1000001A /* GoalEditValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0019C0DE5AFE10000019 /* GoalEditValidationTests.swift */; }; + A11A001CC0DE5AFE1000001C /* GoalPace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001BC0DE5AFE1000001B /* GoalPace.swift */; }; + A11A001EC0DE5AFE1000001E /* GoalPaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001DC0DE5AFE1000001D /* GoalPaceTests.swift */; }; + A11A0020C0DE5AFE10000020 /* Font+Warm.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A001FC0DE5AFE1000001F /* Font+Warm.swift */; }; + A11A0022C0DE5AFE10000022 /* WarmTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0021C0DE5AFE10000021 /* WarmTypeTests.swift */; }; + A11A0024C0DE5AFE10000024 /* KeyboardDismisser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A0023C0DE5AFE10000023 /* KeyboardDismisser.swift */; }; + A11Y0AUD17BUILDFILE00000 /* AccessibilityAuditUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11Y0AUD17FILEREF0000000 /* AccessibilityAuditUITests.swift */; }; + AA28B8C8B38F312A7623F2D2 /* AutoMoveSuggestionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC2151C01A7FBD2947EB51 /* AutoMoveSuggestionTests.swift */; }; + C5DDD46B40453371BD90C01A /* ReceiptScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */; }; + CEBCEA30F32B686EB71A2866 /* ReceiptOCR.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C90599A224DF05C1C7BC95 /* ReceiptOCR.swift */; }; + DE826483AC71FF3EC2A603B7 /* AutoMoveSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F774124079B5BEC3DD37C743 /* AutoMoveSuggestion.swift */; }; + EB84DBEF8B09F9EB1E7AAFF6 /* ReceiptFixtureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6F50EC75E1A4FF9BE4992A /* ReceiptFixtureTests.swift */; }; + ED04D2F84390584EB7EC0615 /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE2C1ED73D49DC33599EF2 /* WelcomeStepView.swift */; }; + F25F23C6EEAA47853BFA137D /* StoreScreenshotTourUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */; }; + F764EBD092BA4435466DD9AA /* AchievementEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12789AD27FF1A7114156F945 /* AchievementEngine.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -123,46 +122,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotSeed.swift; sourceTree = ""; }; - A11Y0AUD17FILEREF0000000 /* AccessibilityAuditUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAuditUITests.swift; sourceTree = ""; }; - 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreScreenshotTourUITests.swift; sourceTree = ""; }; - C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanUITests.swift; sourceTree = ""; }; - 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCRIntegrationTests.swift; sourceTree = ""; }; - D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentCameraView.swift; sourceTree = ""; }; 06A17E22922859025B4DEE1D /* ReceiptScanFlowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanFlowView.swift; sourceTree = ""; }; 0CA5F5E26408477F8E6C59BC /* ReceiptReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptReviewView.swift; sourceTree = ""; }; - 410C0D1F40524DF8CFE25021 /* ReceiptScanModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanModel.swift; sourceTree = ""; }; - D2C90599A224DF05C1C7BC95 /* ReceiptOCR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCR.swift; sourceTree = ""; }; - 9FDC7BEFB3FA01CDB951DBBA /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; - 9C6F50EC75E1A4FF9BE4992A /* ReceiptFixtureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFixtureTests.swift; sourceTree = ""; }; 10A2405B0A3780703DFC27C9 /* ReceiptParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptParserTests.swift; sourceTree = ""; }; - 740B73ADB53C98937307DBAA /* ReceiptParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptParser.swift; sourceTree = ""; }; - 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptDateParser.swift; sourceTree = ""; }; - 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptAmountGrammar.swift; sourceTree = ""; }; - AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptVocabulary.swift; sourceTree = ""; }; - CFA2F094D50821E2DA0497A4 /* OCRLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRLine.swift; sourceTree = ""; }; - 9AFC2151C01A7FBD2947EB51 /* AutoMoveSuggestionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMoveSuggestionTests.swift; sourceTree = ""; }; - F774124079B5BEC3DD37C743 /* AutoMoveSuggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMoveSuggestion.swift; sourceTree = ""; }; - A11A0001C0DE5AFE10000001 /* AmountParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountParsing.swift; sourceTree = ""; }; - A11A0005C0DE5AFE10000005 /* ReminderSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderSettings.swift; sourceTree = ""; }; - A11A000BC0DE5AFE1000000B /* MoneyCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyCategories.swift; sourceTree = ""; }; - A11A000FC0DE5AFE1000000F /* DepositModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepositModel.swift; sourceTree = ""; }; - A11A0011C0DE5AFE10000011 /* GoalDeposits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDeposits.swift; sourceTree = ""; }; - A11A0015C0DE5AFE10000015 /* GoalEditSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditSheet.swift; sourceTree = ""; }; - A11A0017C0DE5AFE10000017 /* GoalEditValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditValidation.swift; sourceTree = ""; }; - A11A001BC0DE5AFE1000001B /* GoalPace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalPace.swift; sourceTree = ""; }; - A11A0023C0DE5AFE10000023 /* KeyboardDismisser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDismisser.swift; sourceTree = ""; }; - A11A0007C0DE5AFE10000007 /* ReminderSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderSettingsTests.swift; sourceTree = ""; }; - A11A000DC0DE5AFE1000000D /* CategoryPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPersistenceTests.swift; sourceTree = ""; }; - A11A0013C0DE5AFE10000013 /* GoalDepositsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDepositsTests.swift; sourceTree = ""; }; - A11A0019C0DE5AFE10000019 /* GoalEditValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditValidationTests.swift; sourceTree = ""; }; - A11A001DC0DE5AFE1000001D /* GoalPaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalPaceTests.swift; sourceTree = ""; }; - A11A0003C0DE5AFE10000003 /* AmountParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountParsingTests.swift; sourceTree = ""; }; - 62FA742D1E24F010F3273E30 /* AchievementEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AchievementEngineTests.swift; sourceTree = ""; }; 12789AD27FF1A7114156F945 /* AchievementEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AchievementEngine.swift; sourceTree = ""; }; - E5BE2C1ED73D49DC33599EF2 /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = ""; }; - F38DAD27B5231BDC080F36DA /* SproutMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SproutMark.swift; sourceTree = ""; }; - 693B815A380D566280140319 /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; 1D3669862CC62F8200E84FF9 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 1D3669882CC62F9100E84FF9 /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; 1D36698A2CC62FD700E84FF9 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -190,7 +153,6 @@ 1DC258042CC07DCC0002DDE3 /* Savely.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Savely.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavelyApp.swift; sourceTree = ""; }; 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - A11A0009C0DE5AFE10000009 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 1DC2580E2CC07DCF0002DDE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 1DC258142CC07DCF0002DDE3 /* SavelyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SavelyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1DC258182CC07DCF0002DDE3 /* SavelyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavelyTests.swift; sourceTree = ""; }; @@ -218,12 +180,49 @@ 1DE9FE162D066EE20069AA64 /* ReportsPDFGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportsPDFGenerator.swift; sourceTree = ""; }; 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrorView.swift; sourceTree = ""; }; 1DF7228D2F9C182300320B38 /* Color+Warm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Warm.swift"; sourceTree = ""; }; - A11A001FC0DE5AFE1000001F /* Font+Warm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Warm.swift"; sourceTree = ""; }; - A11A0021C0DE5AFE10000021 /* WarmTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarmTypeTests.swift; sourceTree = ""; }; 1DF7228F2F9C18FE00320B38 /* GoalDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailView.swift; sourceTree = ""; }; 1DF722912F9C19CE00320B38 /* MoneyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyView.swift; sourceTree = ""; }; 1DF722932F9C19E600320B38 /* AchievementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AchievementsView.swift; sourceTree = ""; }; 1DF722952F9C2FA400320B38 /* AddGoalFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGoalFlow.swift; sourceTree = ""; }; + 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptDateParser.swift; sourceTree = ""; }; + 410C0D1F40524DF8CFE25021 /* ReceiptScanModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanModel.swift; sourceTree = ""; }; + 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreScreenshotTourUITests.swift; sourceTree = ""; }; + 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCRIntegrationTests.swift; sourceTree = ""; }; + 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptAmountGrammar.swift; sourceTree = ""; }; + 62FA742D1E24F010F3273E30 /* AchievementEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AchievementEngineTests.swift; sourceTree = ""; }; + 693B815A380D566280140319 /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; + 740B73ADB53C98937307DBAA /* ReceiptParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptParser.swift; sourceTree = ""; }; + 9AFC2151C01A7FBD2947EB51 /* AutoMoveSuggestionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMoveSuggestionTests.swift; sourceTree = ""; }; + 9C6F50EC75E1A4FF9BE4992A /* ReceiptFixtureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFixtureTests.swift; sourceTree = ""; }; + 9FDC7BEFB3FA01CDB951DBBA /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Fixtures; sourceTree = ""; }; + A11A0001C0DE5AFE10000001 /* AmountParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountParsing.swift; sourceTree = ""; }; + A11A0003C0DE5AFE10000003 /* AmountParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountParsingTests.swift; sourceTree = ""; }; + A11A0005C0DE5AFE10000005 /* ReminderSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderSettings.swift; sourceTree = ""; }; + A11A0007C0DE5AFE10000007 /* ReminderSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderSettingsTests.swift; sourceTree = ""; }; + A11A0009C0DE5AFE10000009 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; + A11A000BC0DE5AFE1000000B /* MoneyCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyCategories.swift; sourceTree = ""; }; + A11A000DC0DE5AFE1000000D /* CategoryPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPersistenceTests.swift; sourceTree = ""; }; + A11A000FC0DE5AFE1000000F /* DepositModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepositModel.swift; sourceTree = ""; }; + A11A0011C0DE5AFE10000011 /* GoalDeposits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDeposits.swift; sourceTree = ""; }; + A11A0013C0DE5AFE10000013 /* GoalDepositsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDepositsTests.swift; sourceTree = ""; }; + A11A0015C0DE5AFE10000015 /* GoalEditSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditSheet.swift; sourceTree = ""; }; + A11A0017C0DE5AFE10000017 /* GoalEditValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditValidation.swift; sourceTree = ""; }; + A11A0019C0DE5AFE10000019 /* GoalEditValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalEditValidationTests.swift; sourceTree = ""; }; + A11A001BC0DE5AFE1000001B /* GoalPace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalPace.swift; sourceTree = ""; }; + A11A001DC0DE5AFE1000001D /* GoalPaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalPaceTests.swift; sourceTree = ""; }; + A11A001FC0DE5AFE1000001F /* Font+Warm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Warm.swift"; sourceTree = ""; }; + A11A0021C0DE5AFE10000021 /* WarmTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarmTypeTests.swift; sourceTree = ""; }; + A11A0023C0DE5AFE10000023 /* KeyboardDismisser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDismisser.swift; sourceTree = ""; }; + A11Y0AUD17FILEREF0000000 /* AccessibilityAuditUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAuditUITests.swift; sourceTree = ""; }; + AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptVocabulary.swift; sourceTree = ""; }; + C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanUITests.swift; sourceTree = ""; }; + CFA2F094D50821E2DA0497A4 /* OCRLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCRLine.swift; sourceTree = ""; }; + D2C90599A224DF05C1C7BC95 /* ReceiptOCR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCR.swift; sourceTree = ""; }; + D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentCameraView.swift; sourceTree = ""; }; + E5BE2C1ED73D49DC33599EF2 /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = ""; }; + F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotSeed.swift; sourceTree = ""; }; + F38DAD27B5231BDC080F36DA /* SproutMark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SproutMark.swift; sourceTree = ""; }; + F774124079B5BEC3DD37C743 /* AutoMoveSuggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMoveSuggestion.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -251,18 +250,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 5A440101E954A364F752379E /* Receipt */ = { - isa = PBXGroup; - children = ( - 740B73ADB53C98937307DBAA /* ReceiptParser.swift */, - 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */, - 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */, - AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */, - CFA2F094D50821E2DA0497A4 /* OCRLine.swift */, - ); - path = Receipt; - sourceTree = ""; - }; 1D3669842CC62EDD00E84FF9 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -606,6 +593,18 @@ path = Dashboard; sourceTree = ""; }; + 5A440101E954A364F752379E /* Receipt */ = { + isa = PBXGroup; + children = ( + 740B73ADB53C98937307DBAA /* ReceiptParser.swift */, + 20B90A9EEB228FE2A1132DFD /* ReceiptDateParser.swift */, + 505D129592F5E1D668ECF4C3 /* ReceiptAmountGrammar.swift */, + AEA22FF0B24D8017909BA914 /* ReceiptVocabulary.swift */, + CFA2F094D50821E2DA0497A4 /* OCRLine.swift */, + ); + path = Receipt; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -718,7 +717,6 @@ 1DC2580F2CC07DCF0002DDE3 /* Preview Assets.xcassets in Resources */, 1DC2580C2CC07DCF0002DDE3 /* Assets.xcassets in Resources */, A11A000AC0DE5AFE1000000A /* AppIcon.icon in Resources */, - 1DAF5D242CEE9AF4001C0AF3 /* Config.plist in Resources */, 1D36698B2CC62FD700E84FF9 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1032,8 +1030,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.Savely; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1068,8 +1068,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.Savely; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1090,8 +1092,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.SavelyTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1113,8 +1117,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.SavelyTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1134,8 +1140,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.SavelyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1155,8 +1163,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = IvanLB.SavelyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SUPPORTS_MACCATALYST = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1204,8 +1214,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - - }; rootObject = 1DC257FC2CC07DCC0002DDE3 /* Project object */; } diff --git a/Savely/Info.plist b/Savely/Info.plist index 17bc9a4..cbb14da 100644 --- a/Savely/Info.plist +++ b/Savely/Info.plist @@ -2,27 +2,10 @@ - + ITSAppUsesNonExemptEncryption - NSAppTransportSecurity - - NSExceptionDomains - - openai.com - - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - NSTemporaryExceptionMinimumTLSVersion - TLSv1.2 - - - diff --git a/SavelyUITests/StoreScreenshotTourUITests.swift b/SavelyUITests/StoreScreenshotTourUITests.swift index 4a9696b..3f4b786 100644 --- a/SavelyUITests/StoreScreenshotTourUITests.swift +++ b/SavelyUITests/StoreScreenshotTourUITests.swift @@ -128,6 +128,123 @@ final class StoreScreenshotTourUITests: XCTestCase { snap("00-home-light") } + /// App Preview footage: one continuous, human-paced walk through the + /// seeded app while `simctl io … recordVideo` captures the screen. Prints + /// `BRAGMARK [x y]` lines (points) so the editor can cut + /// scenes and draw touch indicators without guessing. + @MainActor + func testPreviewFootage() throws { + launch() + // The first launch asks for notifications; answer it before the take. + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + if springboard.buttons["Allow"].waitForExistence(timeout: 3) { + springboard.buttons["Allow"].tap() + } + pause(1.0) + mark("home") + pause(2.0) + + tap(app.buttons["Goals"].firstMatch, "goals-tab") + let oaxaca = app.staticTexts["Trip to Oaxaca"] + XCTAssertTrue(oaxaca.waitForExistence(timeout: 8)) + pause(0.8) + tap(oaxaca, "goal-open") + XCTAssertTrue(app.navigationBars["Goal"].waitForExistence(timeout: 8)) + mark("goal-before") + pause(3.0) + tap(app.navigationBars.buttons.firstMatch, "goal-back") + pause(0.8) + + tap(app.buttons["Add"], "add-1") + let expense = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log expense'")).firstMatch + XCTAssertTrue(expense.waitForExistence(timeout: 5)) + pause(0.6) + tap(expense, "expense-open") + XCTAssertTrue(app.staticTexts["Log expense"].waitForExistence(timeout: 5)) + pause(0.5) + for key in ["4", "Decimal point", "5", "0"] { tap(app.buttons[key].firstMatch, "key-\(key)"); pause(0.15) } + pause(0.3) + tap(app.buttons["Coffee"].firstMatch, "expense-coffee") + pause(1.2) + tap(app.buttons["Save"].firstMatch, "expense-save") + pause(1.5) + + tap(app.buttons["Add"], "add-2") + let income = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log income'")).firstMatch + XCTAssertTrue(income.waitForExistence(timeout: 5)) + pause(0.6) + tap(income, "income-open") + XCTAssertTrue(app.staticTexts["Log income"].waitForExistence(timeout: 5)) + pause(0.5) + for key in ["1", "2", "4", "0"] { tap(app.buttons[key].firstMatch, "key-\(key)"); pause(0.15) } + XCTAssertTrue(app.buttons["YES"].waitForExistence(timeout: 3)) + mark("income-banner") + pause(1.6) + tap(app.buttons["YES"], "income-yes") + pause(1.4) + tap(app.buttons["Save"].firstMatch, "income-save") + pause(1.2) + + tap(app.buttons["Goals"].firstMatch, "goals-tab-2") + XCTAssertTrue(oaxaca.waitForExistence(timeout: 8)) + pause(0.8) + tap(oaxaca, "goal-open-2") + XCTAssertTrue(app.navigationBars["Goal"].waitForExistence(timeout: 8)) + mark("goal-after") + pause(3.0) + tap(app.navigationBars.buttons.firstMatch, "goal-back-2") + pause(0.6) + + tap(app.buttons["Money"].firstMatch, "money-tab") + let incomeSeg = app.buttons["Income"].firstMatch + XCTAssertTrue(incomeSeg.waitForExistence(timeout: 8)) + pause(1.0) + tap(incomeSeg, "money-income") + mark("trend") + pause(3.0) + + tap(app.buttons["Me"].firstMatch, "me-tab") + pause(0.8) + for _ in 0..<9 { app.swipeUp() } + XCTAssertTrue(app.staticTexts["Your data never leaves this iPhone."].waitForExistence(timeout: 5)) + mark("privacy") + pause(2.5) + + tap(app.buttons["Add"], "add-3") + let scan = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Scan a receipt'")).firstMatch + XCTAssertTrue(scan.waitForExistence(timeout: 5)) + pause(0.6) + tap(scan, "scan-open") + let picker = app.buttons["Choose from Photos"] + XCTAssertTrue(picker.waitForExistence(timeout: 8)) + pause(0.8) + tap(picker, "scan-photos") + let photo = app.images.matching(NSPredicate(format: "label BEGINSWITH 'Photo'")).firstMatch + guard photo.waitForExistence(timeout: 30) else { mark("receipt-missing"); return } + pause(0.8) + tap(photo, "scan-photo") + XCTAssertTrue(app.staticTexts["Check the receipt"].waitForExistence(timeout: 30)) + mark("receipt") + pause(3.5) + mark("end") + } + + private func mark(_ name: String, _ point: CGPoint? = nil) { + let t = String(format: "%.3f", Date().timeIntervalSince1970) + let p = point.map { String(format: " %.1f %.1f", $0.x, $0.y) } ?? "" + print("BRAGMARK \(name) \(t)\(p)") + } + + private func tap(_ element: XCUIElement, _ name: String) { + let frame = element.frame + mark(name, CGPoint(x: frame.midX, y: frame.midY)) + element.tap() + } + + private func pause(_ seconds: TimeInterval) { + Thread.sleep(forTimeInterval: seconds) + } + private func snap(_ name: String) { let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = name diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 2303b38..d676ced 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -3,17 +3,16 @@ # ci_scripts/ci_post_clone.sh — Xcode Cloud post-clone hook for Savely. # # WHY IT EXISTS -# A fresh clone of this repo is missing exactly one file xcodebuild insists -# on: Savely/Config.plist. It is gitignored (it carries the OpenAI key for -# the Tips feature) but the Xcode project lists it as a Resources build -# input, so on a clean checkout xcodebuild stops with "Build input file -# cannot be found". .github/workflows/ci.yml solves this on GitHub Actions -# by writing a placeholder; this script is the same idea for Xcode Cloud, -# plus one honesty gate for archives (see section 2). +# Until 1.1 this script wrote Savely/Config.plist (the OpenAI key for the +# Tips feature), because the Xcode project listed it as a Resources build +# input and a clean checkout would not build without it. That entry was the +# bug: anything in Resources ships inside the IPA, and a key in a bundled +# plist can be lifted out by anyone (docs/plans/drop-bundled-config-plist.md). +# Config.plist is no longer a build input, so nothing needs materialising. # -# Unlike Fave (docs/xcode-cloud.md over there is the origin of this file), -# Savely commits its .xcodeproj and its shared scheme, so there is no -# project generation step here. Everything about XcodeGen was dropped. +# What is left is one honesty gate for archives: the Tips feature has no +# safe way to get a key today, so an archive with FeatureFlags.tipsEnabled +# set to true would ship a Tips card that silently never loads. Refuse it. # # APPLE'S CONTRACT — the parts this script leans on, from # developer.apple.com/documentation/xcode/writing-custom-build-scripts and @@ -27,25 +26,12 @@ # · The clone is at $CI_PRIMARY_REPOSITORY_PATH. # · A nonzero exit fails the build, which is what every failure path here # does, loudly, instead of letting xcodebuild fail later. -# · Writes into the clone survive into the xcodebuild step. -# -# SECRET DISCIPLINE (house rule: env var only, never committed, never logged) -# If a real key is provided it arrives as the SECRET workflow environment -# variable named in $SECRET_ENV_VAR. It is never expanded into a command -# argument, an assignment, or an echo: `printenv` writes it straight down a -# pipe, so even under `set -x` the trace shows the word "printenv" and not -# the value. Every check on the resulting file uses `grep -c`/`grep -q`, -# never a form that could print the line. # set -euo pipefail set +x # ---------------------------------------------------------------- constants -- -# The workflow environment variable that carries the OpenAI key. Must be -# marked SECRET in the Xcode Cloud workflow's Environment section. -readonly SECRET_ENV_VAR="OPENAI_API_KEY" -readonly PLIST_REL="Savely/Config.plist" readonly FLAGS_REL="Savely/Utilities/FeatureFlags.swift" # ------------------------------------------------------------------ helpers -- @@ -72,30 +58,8 @@ cd "$repo_root" log "repo root: $repo_root" [ -f "$FLAGS_REL" ] || fail "$FLAGS_REL missing; the archive gate below reads it" -# --------------------------------------------- 2. materialise Config.plist -- -# Three states, all of them valid: -# a) variable set and non-empty -> write the real key -# b) variable set but empty -> write the placeholder -# c) variable not set at all -> write the placeholder -# The placeholder is what a fresh local clone and GitHub Actions build with: -# the app runs, OpenAIClient.init? finds a string that is not a key, and the -# Tips call answers nothing. That is fine while the Tips feature is OFF. - -key_state="absent" -if printenv "$SECRET_ENV_VAR" >/dev/null 2>&1; then - key_state="empty" - key_chars="$(printenv "$SECRET_ENV_VAR" | tr -d '[:space:]' | wc -c | tr -d '[:space:]')" - if [ "${key_chars:-0}" -gt 0 ]; then - key_state="present" - fi -fi +# ------------------------------------------------ 2. the Tips archive gate -- -# The Tips feature is gated by FeatureFlags.tipsEnabled. While it is `false` -# the shipped app never touches OpenAI and a placeholder key is not merely -# acceptable, it is preferable: a real key inside a plist in the bundle can be -# lifted out of the IPA. The moment someone flips the flag on, an ARCHIVE -# without a real key would ship a Tips card that silently never loads, with a -# green checkmark on the workflow. Refuse that combination and nothing else. tips_enabled="unknown" if grep -Eq '^\s*static let tipsEnabled\s*=\s*true\b' "$FLAGS_REL"; then tips_enabled="true" @@ -105,50 +69,20 @@ fi [ "$tips_enabled" != "unknown" ] \ || fail "could not read FeatureFlags.tipsEnabled from $FLAGS_REL; the line changed shape, update the grep here" -if [ "${CI_XCODEBUILD_ACTION:-}" = "archive" ] && [ "$tips_enabled" = "true" ] && [ "$key_state" != "present" ]; then - fail "\$$SECRET_ENV_VAR is $key_state, CI_XCODEBUILD_ACTION=archive and FeatureFlags.tipsEnabled is true. - Refusing to ship a release with the Tips feature on and no OpenAI key: the - card would never load and nothing would say so. - Either add $SECRET_ENV_VAR as a SECRET environment variable on the workflow - (Xcode 26/27: Integrate > Manage Workflows > Environment) or turn the flag off." -fi - -mkdir -p "$(dirname "$PLIST_REL")" -umask 077 -{ - printf '\n' - printf '\n' - printf '\n\n' - printf '\t%s\n' "$SECRET_ENV_VAR" - printf '\t' - if [ "$key_state" = "present" ]; then - # Whitespace stripped (a stray CR pasted into the App Store Connect field - # would otherwise become part of the key), then XML-escaped. The value - # never binds to a shell variable on the way. - printenv "$SECRET_ENV_VAR" | tr -d '[:space:]' | sed -e 's/&/\&/g' -e 's//\>/g' - else - printf 'xcode-cloud-placeholder-not-a-real-key' - fi - printf '\n\n\n' -} > "$PLIST_REL" -chmod 600 "$PLIST_REL" - -if [ "$key_state" = "present" ]; then - log "$PLIST_REL written from \$$SECRET_ENV_VAR (value not logged)" -else - log "\$$SECRET_ENV_VAR is $key_state; $PLIST_REL written with a placeholder (tipsEnabled=$tips_enabled, action=${CI_XCODEBUILD_ACTION:-})" +if [ "${CI_XCODEBUILD_ACTION:-}" = "archive" ] && [ "$tips_enabled" = "true" ]; then + fail "CI_XCODEBUILD_ACTION=archive and FeatureFlags.tipsEnabled is true. + Refusing to ship the Tips feature: it has no safe key source. A bundled + Config.plist is equivalent to publishing the key; serve it from a proxy + you control first (docs/plans/drop-bundled-config-plist.md, step 3), + then update this gate." fi -# ------------------------------------------------------------- 3. verify it -- -# plutil -lint prints only OK / the error, never the values. -plutil -lint "$PLIST_REL" >/dev/null || fail "$PLIST_REL is not a valid plist" -[ "$(grep -c "${SECRET_ENV_VAR}" "$PLIST_REL")" -eq 1 ] \ - || fail "$PLIST_REL does not contain exactly one ${SECRET_ENV_VAR} entry" +# ------------------------------------------------------------ 3. sanity check -- if [ -n "${CI_PROJECT_FILE_PATH:-}" ] && [ ! -e "$CI_PROJECT_FILE_PATH" ]; then fail "Xcode Cloud expects the project at CI_PROJECT_FILE_PATH='$CI_PROJECT_FILE_PATH', which does not exist. Recreate the workflow against Savely.xcodeproj at the repo root." fi -log "done — $PLIST_REL in place." +log "done (tipsEnabled=$tips_enabled, action=${CI_XCODEBUILD_ACTION:-})." exit 0 diff --git a/docs/plans/dead-api-records.md b/docs/plans/dead-api-records.md new file mode 100644 index 0000000..026dfc5 --- /dev/null +++ b/docs/plans/dead-api-records.md @@ -0,0 +1,74 @@ +# Dead API records still in the project + +> Found 2026-08-20 while grounding atelierbelli.com/savely/support in what the +> app actually does. Companion to `drop-bundled-config-plist.md`, which covers +> the third record (the bundled OpenAI plist) on its own. +> Status: **§1 and the on-disk half of §2 DONE in 1.1 (branch +> `chore/release-hygiene`)** — ATS block removed, GoogleService-Info.plist +> deleted locally. **Still open, owner-only:** the Firebase project +> `savely-5160e` (§2, "How much this actually matters") — confirm it is dormant +> and delete it in the Firebase console. + +Savely 1.0 talks to nothing. The OpenAI tips feature is unreachable +(`FeatureFlags.tipsEnabled = false`, `Savely/Utilities/FeatureFlags.swift:22`) +and the Firebase auth stack was removed in 2026-08. What is left is paperwork +that still describes integrations the app no longer has. + +## 1. ATS exception for openai.com + +`Savely/Info.plist:10-26` still declares an `NSAppTransportSecurity` +exception-domain block for `openai.com`. It grants nothing dangerous +(`NSExceptionAllowsInsecureHTTPLoads` is `false`), but it is statically visible +to App Review and it advertises a network integration in an app whose support +page and App Privacy answers both say nothing leaves the phone. + +**Chore:** delete the whole `NSAppTransportSecurity` dict. Keep +`ITSAppUsesNonExemptEncryption` — that one is load-bearing for upload. +Restore the exception only if the tips feature is ever re-enabled. + +## 2. GoogleService-Info.plist + +Two separate facts, and only the second one matters: + +- **On disk:** `Savely/GoogleService-Info.plist` still exists but is inert. It + is not referenced by `project.pbxproj` and not imported by any `.swift` file + (verified by grep), so it does **not** ship in the binary. It is leftover + weight from the removed auth stack. Safe to delete whenever. +- **In git history:** it was committed in `80cf033` ("Implementation of + Firebase to the Project") and the blob is still reachable + (`git cat-file -p 311b1042689098ca3a8e97e432f3a1c560cabb18`). `.gitignore` + only stopped future tracking. **`github.com/Ivan-LB/Savely` is a public + repo**, so the contents are publicly readable today. + +### How much this actually matters + +Less than it looks, but not zero, and **this key was never revoked** — it is +not the OpenAI key Iván killed on 2026-08-19, it is a separate Firebase +credential for project `savely-5160e`. + +A Firebase iOS `API_KEY` is not a secret in the classic sense: Google documents +it as a project identifier, not an authorization token. Real protection comes +from Firebase Security Rules, API-key restrictions, and App Check, not from +hiding the string. So the exposure is closer to "project identifiers are +public" than "credentials leaked". + +What still deserves a look, in rough priority order: + +1. **Confirm the Firebase project is actually dormant.** If `savely-5160e` has + no active Auth users, no Firestore/Storage data, and no billing, the + exposure is academic and deleting the whole project is the cleanest fix. +2. **If the project stays alive**, add an API-key restriction (bundle-id + allowlist) in Google Cloud Console → APIs & Services → Credentials, and + confirm Security Rules are not left in test mode. +3. **Only then consider a history rewrite.** `git filter-repo` plus a + force-push would purge the blob, but it rewrites every commit hash, breaks + existing clones, and touches a repo wired to Xcode Cloud. Given the key is a + project identifier and the project is likely dormant, deleting the Firebase + project outright is a better fix than rewriting history. + +**Do not** treat "the key was revoked" as covering this one. It was not. + +## 3. Bundled Config.plist + +See `drop-bundled-config-plist.md`. Key revoked 2026-08-19; the build-phase +entry is the remaining bug. diff --git a/docs/plans/drop-bundled-config-plist.md b/docs/plans/drop-bundled-config-plist.md new file mode 100644 index 0000000..797bc1b --- /dev/null +++ b/docs/plans/drop-bundled-config-plist.md @@ -0,0 +1,41 @@ +# Drop the bundled Config.plist from Resources + +> Found 2026-08-19 during an external privacy review of all shipping apps. +> Status: **DONE in 1.1 (branch `chore/release-hygiene`)** — steps 1–2 shipped, +> the CI placeholder steps (ci.yml, ci_post_clone.sh) were retired with it and the +> Xcode Cloud gate now refuses any archive with Tips on. Step 3 stands for when +> Tips returns. Key revoked by Iván 2026-08-19. + +## What happened + +`Savely/Config.plist` holds the OpenAI API key for the tips feature and is +**bundled into the app**: it appears in the Resources build phase at +`Savely.xcodeproj/project.pbxproj:27` and `:721`, loaded at +`Savely/Utilities/OpenAIClient.swift:16-22` via +`Bundle.main.path(forResource: "Config")`. It is gitignored +(`.gitignore:14`) so it never reached git history, but anything in Resources +is extractable from the .ipa by anyone. The build submitted to App Review +carried a live key. The feature itself is unreachable +(`FeatureFlags.tipsEnabled = false`, `Savely/Utilities/FeatureFlags.swift:22`) +but the plist ships regardless. + +Iván revoked the key in the OpenAI account on 2026-08-19, so the shipped copy +is dead. Remaining work is hygiene: + +## The chore + +1. Remove `Config.plist` from the Resources build phase (keep the file on disk + and gitignored for local dev when the feature returns). +2. Verify: `grep -n "Config.plist" Savely.xcodeproj/project.pbxproj` shows no + `in Resources` entry, the app still builds, and a fresh archive's .app + bundle contains no Config.plist (`unzip -l` the .ipa or inspect the archive). +3. When the tips feature is ever turned back on: the key must NOT return via + bundled plist. Ship it behind a proxy you control, or at minimum load from + Keychain after first launch. A bundled plist is equivalent to publishing + the key. + +## Why not urgent anymore + +Revoked key = dead file. But the pattern regenerates the incident the moment +someone rotates the key locally and archives: the build phase entry is the +bug, not the key value. diff --git a/docs/xcode-cloud.md b/docs/xcode-cloud.md index 2f03ea2..ca13568 100644 --- a/docs/xcode-cloud.md +++ b/docs/xcode-cloud.md @@ -16,18 +16,15 @@ different for Savely and the settings that matter. Where Fave's notes say | Fave | Savely | |---|---| | Project is generated by XcodeGen, not committed | `Savely.xcodeproj` and its shared scheme are committed. No generation step, no XcodeGen, no scheme patch. | -| Secret is `Config/Secrets.xcconfig` (TMDB key), gitignored | Secret is `Savely/Config.plist` (OpenAI key), gitignored, **but registered as a Resources build input**, so a clean checkout fails to build without it. | -| An archive without the key is refused | The Tips feature is behind `FeatureFlags.tipsEnabled = false`, so the shipped app never calls OpenAI and a placeholder is preferable (a real key in a bundled plist can be lifted from the IPA). The archive is refused only if the flag is `true` AND no key was provided. | +| Secret is `Config/Secrets.xcconfig` (TMDB key), gitignored | No build-time secret. `Savely/Config.plist` (OpenAI key, gitignored) stopped being a Resources build input in 1.1, so a clean checkout builds as-is. | +| An archive without the key is refused | The Tips feature is behind `FeatureFlags.tipsEnabled = false` and has no safe key source (a key in a bundled plist can be lifted from the IPA). Any archive with the flag `true` is refused. | -`ci_scripts/ci_post_clone.sh` writes `Savely/Config.plist` from the workflow -variable `OPENAI_API_KEY` if it exists, or a placeholder if it does not, and -enforces that one gate. It was dry-run against a scratch clone with a canary -key containing `&<>` under `bash -x`: zero hits in the trace, one hit in the -plist, XML-escaped, `plutil -lint` OK, and the tips-on/no-key/archive -combination exits 1 with a message that names the fix. +`ci_scripts/ci_post_clone.sh` now only enforces that gate. Until 1.1 it also +wrote `Savely/Config.plist` from a SECRET `OPENAI_API_KEY` variable (or a +placeholder); that ended with `docs/plans/drop-bundled-config-plist.md`. -The workflow does not need any environment variable for a 1.0 archive. Add -`OPENAI_API_KEY` as a SECRET variable only when the flag flips on. +The workflow needs no environment variables. Do not add `OPENAI_API_KEY` +back: when Tips returns, the key lives behind a proxy, not in the build. ## Workflow settings