From b928a56fe2a160d36908dfd6488ca6abc5bf45a5 Mon Sep 17 00:00:00 2001 From: Belli Date: Mon, 28 Sep 2026 19:23:10 -0700 Subject: [PATCH 1/4] chore(build): stop bundling Config.plist; iPhone-only on Mac and Vision Pro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config.plist sat in Copy Bundle Resources, so whatever key was on disk at archive time shipped inside the IPA (1.0's key was revoked 2026-08-19). The file stays on disk and gitignored for local dev; it is simply no longer a build input. - Drop the Resources build-phase entry (file reference kept). - Retire the placeholder-plist steps in ci.yml and ci_post_clone.sh; a clean checkout now builds without it. - ci_post_clone.sh keeps one gate: refuse any archive with FeatureFlags.tipsEnabled = true, since Tips has no safe key source yet. - New invariant: gotchas.yaml#config-plist-never-bundled, CLAUDE.md §3. - Pending Xcode change: SUPPORTS_MACCATALYST and the "Designed for iPhone/iPad" on Mac and Vision Pro destinations set to NO, plus Xcode's re-sort of the PBXBuildFile/PBXFileReference sections. --- .claude/knowledge/gotchas.yaml | 10 +- .github/workflows/ci.yml | 18 -- CLAUDE.md | 4 +- Savely.xcodeproj/project.pbxproj | 224 ++++++++++++------------ ci_scripts/ci_post_clone.sh | 102 ++--------- docs/plans/drop-bundled-config-plist.md | 41 +++++ docs/xcode-cloud.md | 17 +- 7 files changed, 189 insertions(+), 227 deletions(-) create mode 100644 docs/plans/drop-bundled-config-plist.md 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/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/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/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 From 0c6696217662b723ddd2e7fc5295bf84db487ad3 Mon Sep 17 00:00:00 2001 From: Belli Date: Mon, 28 Sep 2026 19:23:10 -0700 Subject: [PATCH 2/4] chore: drop the openai.com ATS exception from Info.plist Savely 1.0 makes no network calls (Tips is flagged off), but Info.plist still declared an App Transport Security exception for openai.com, visible to App Review and at odds with the "Data Not Collected" privacy answers. ITSAppUsesNonExemptEncryption stays; it is load-bearing for upload. The leftover GoogleService-Info.plist was deleted locally (gitignored, never bundled). The Firebase project itself is still open, owner-only; see docs/plans/dead-api-records.md. --- Savely/Info.plist | 23 ++--------- docs/plans/dead-api-records.md | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 docs/plans/dead-api-records.md 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/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. From fa32d83d3a07834f7ed11d09a00dc6c7cdad2357 Mon Sep 17 00:00:00 2001 From: Belli Date: Mon, 28 Sep 2026 19:23:35 -0700 Subject: [PATCH 3/4] chore(store): add an App Preview footage walk to the screenshot tour testPreviewFootage walks the seeded app at human pace (home, goal, expense, payday auto-move, goal again, trend, privacy, receipt) while `simctl io recordVideo` captures it, and prints BRAGMARK lines with timestamps and tap centers so an editor can cut scenes and draw touch indicators. It also answers the first-launch notification prompt. --- .../StoreScreenshotTourUITests.swift | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) 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 From c096ad7c4dd27535673b8afe56873392658d9d2c Mon Sep 17 00:00:00 2001 From: Belli Date: Mon, 28 Sep 2026 19:23:35 -0700 Subject: [PATCH 4/4] chore: ignore /brag launch-video output --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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*/