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' -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