|
| 1 | +# Builder Init — single-app-pivot |
| 2 | + |
| 3 | +## Current File Structure |
| 4 | + |
| 5 | +``` |
| 6 | +ios/ |
| 7 | +├── Shared/ |
| 8 | +│ ├── Config.swift — App-wide constants (backendBaseURL = "https://piper.workers.dev") |
| 9 | +│ ├── CookieManager.swift — Uses AppGroupStorage (UserDefaults suiteName: "group.com.piper.app") |
| 10 | +│ └── Models.swift — ConnectionState, ExtractedContent, SaveResponse |
| 11 | +├── Piper/ |
| 12 | +│ ├── PiperApp.swift — @main, creates CookieManager() and passes to ContentView |
| 13 | +│ ├── ContentView.swift — Shows connect/connected states, no "Pipe Article" button yet |
| 14 | +│ ├── XLoginView.swift — WKWebView login sheet, detects x.com/home redirect |
| 15 | +│ └── Piper.entitlements — Contains com.apple.security.application-groups |
| 16 | +├── PiperShareExtension/ |
| 17 | +│ ├── ShareViewController.swift — UIKit orchestrator: cookies → extract → POST → clipboard |
| 18 | +│ ├── PiperShareExtension.entitlements — App Group entitlement |
| 19 | +│ ├── readability.js — Mozilla Readability (real, not stub) |
| 20 | +│ └── Services/ |
| 21 | +│ ├── ContentExtractor.swift — WKWebView + readability.js extraction |
| 22 | +│ └── PiperAPIClient.swift — HTTP POST /save client |
| 23 | +├── PiperShareExtensionTests/ |
| 24 | +│ ├── ContentExtractorTests.swift — imports @testable PiperShareExtension |
| 25 | +│ ├── PiperAPIClientTests.swift — imports @testable PiperShareExtension |
| 26 | +│ ├── ShareViewControllerTests.swift — imports @testable PiperShareExtension (DELETE) |
| 27 | +│ └── ConfigTests.swift — imports @testable PiperShareExtension |
| 28 | +└── PiperTests/ |
| 29 | + ├── CookieManagerTests.swift — imports @testable Piper, uses InMemoryStorage mock |
| 30 | + ├── ContentViewTests.swift — imports @testable Piper |
| 31 | + └── LoginDetectionTests.swift — imports @testable Piper |
| 32 | +``` |
| 33 | + |
| 34 | +Note: No Xcode project file (.xcodeproj) is present in the repo — the project is managed |
| 35 | +separately (likely created manually in Xcode). The repo only contains the Swift source files. |
| 36 | +This means ALL Xcode project changes (target membership, build phases) must be done via |
| 37 | +direct file edits to the source tree. The absence of a .xcodeproj means tests are likely |
| 38 | +run via an Xcode project that exists only on developer machines. |
| 39 | + |
| 40 | +## What Needs to Change |
| 41 | + |
| 42 | +### Files to Modify |
| 43 | + |
| 44 | +1. **ios/Shared/CookieManager.swift** |
| 45 | + - Remove `AppGroupStorage` class (uses `UserDefaults(suiteName:)` which requires entitlements) |
| 46 | + - Replace with `StandardStorage` that wraps `UserDefaults.standard` |
| 47 | + - Update `convenience init()` to use `StandardStorage()` instead of `AppGroupStorage()` |
| 48 | + - Update comment: "App Group" → "standard UserDefaults" |
| 49 | + - The `CookieStorage` protocol and `CookieManager` class remain structurally identical |
| 50 | + - CookieManagerTests already uses `InMemoryStorage` mock — no test changes needed for |
| 51 | + the storage-switch itself, but a new test verifying StandardStorage uses UserDefaults.standard |
| 52 | + is required per spec |
| 53 | + |
| 54 | +2. **ios/Piper/ContentView.swift** |
| 55 | + - Add "Pipe Article" button in the `.connected` case |
| 56 | + - Add a `pipelineController` dependency (injected) |
| 57 | + - Add state for showing PipeView sheet: `@State private var showingPipeView = false` |
| 58 | + - Add state for clipboard URL and error messages |
| 59 | + - The connected section currently shows: "You're all set. Use the share sheet to pipe articles." |
| 60 | + → Replace with "Pipe Article" button + disconnect button |
| 61 | + - Wire to PipeView sheet |
| 62 | + |
| 63 | +3. **ios/PiperTests/CookieManagerTests.swift** |
| 64 | + - Add Test 1: verify that `CookieManager()` (convenience init) uses standard UserDefaults, |
| 65 | + not an App Group. This likely tests that keys are stored under `UserDefaults.standard` |
| 66 | + |
| 67 | +### Files to Move (content copy + import update) |
| 68 | + |
| 69 | +4. **ios/PiperShareExtension/Services/ContentExtractor.swift → ios/Piper/Services/ContentExtractor.swift** |
| 70 | + - Change `@testable import PiperShareExtension` → `@testable import Piper` in tests |
| 71 | + - Change error message: "readability.js resource is missing from the extension bundle" → |
| 72 | + "readability.js resource is missing from the app bundle" |
| 73 | + - `Bundle(for: ContentExtractor.self)` will now resolve to the Piper app bundle |
| 74 | + |
| 75 | +5. **ios/PiperShareExtension/Services/PiperAPIClient.swift → ios/Piper/Services/PiperAPIClient.swift** |
| 76 | + - No content changes needed (uses Config.backendBaseURL from Shared/) |
| 77 | + |
| 78 | +6. **ios/PiperShareExtension/readability.js → ios/Piper/Resources/readability.js** |
| 79 | + - Pure file move, no content changes |
| 80 | + |
| 81 | +7. **ios/PiperShareExtensionTests/ContentExtractorTests.swift → ios/PiperTests/ContentExtractorTests.swift** |
| 82 | + - Change `@testable import PiperShareExtension` → `@testable import Piper` |
| 83 | + |
| 84 | +8. **ios/PiperShareExtensionTests/PiperAPIClientTests.swift → ios/PiperTests/PiperAPIClientTests.swift** |
| 85 | + - Change `@testable import PiperShareExtension` → `@testable import Piper` |
| 86 | + |
| 87 | +9. **ios/PiperShareExtensionTests/ConfigTests.swift → ios/PiperTests/ConfigTests.swift** |
| 88 | + - Change `@testable import PiperShareExtension` → `@testable import Piper` |
| 89 | + |
| 90 | +### Files to Create |
| 91 | + |
| 92 | +10. **ios/Piper/Services/PipelineController.swift** |
| 93 | + - Protocol-based dependencies: `ContentExtracting`, `PiperAPIClientProtocol`, `CookieManager` |
| 94 | + - Method: `pipe(urlString: String, completion: @escaping (Result<String, Error>) -> Void)` |
| 95 | + - Error cases: notLoggedIn, invalidURL, extractionFailed(Error), saveFailed(Error) |
| 96 | + - Orchestration: check cookies → validate URL → extract → save → return result URL string |
| 97 | + - No UI dependencies — pure service class |
| 98 | + - Should be async-friendly (completion handlers) |
| 99 | + |
| 100 | +11. **ios/Piper/PipeView.swift** |
| 101 | + - SwiftUI view showing extraction progress/result |
| 102 | + - Receives a URL string from clipboard |
| 103 | + - Uses `PipelineController` to run the pipe flow |
| 104 | + - Shows: loading spinner → success ("Saved — paste into Instapaper") or error |
| 105 | + - No direct network or storage access |
| 106 | + |
| 107 | +12. **ios/PiperTests/PipelineControllerTests.swift** |
| 108 | + - Test 1: No cookies → returns notLoggedIn error |
| 109 | + - Test 2: Happy path → valid cookies + mock extractor + mock API → returns success URL |
| 110 | + - Test 3: Extraction failure → returns extraction error |
| 111 | + - Test 4: API failure → extraction succeeds + API fails → returns save error |
| 112 | + - Test 5: Invalid URL → returns invalidURL error |
| 113 | + |
| 114 | +### Files to Delete |
| 115 | + |
| 116 | +13. **ios/PiperShareExtension/** (entire directory) |
| 117 | + - ShareViewController.swift |
| 118 | + - PiperShareExtension.entitlements |
| 119 | + - readability.js |
| 120 | + - Services/ContentExtractor.swift |
| 121 | + - Services/PiperAPIClient.swift |
| 122 | + |
| 123 | +14. **ios/PiperShareExtensionTests/** (entire directory) |
| 124 | + - ContentExtractorTests.swift (moved to PiperTests) |
| 125 | + - PiperAPIClientTests.swift (moved to PiperTests) |
| 126 | + - ShareViewControllerTests.swift (deleted — no replacement, tests covered by PipelineControllerTests) |
| 127 | + - ConfigTests.swift (moved to PiperTests) |
| 128 | + |
| 129 | +15. **ios/Piper/Piper.entitlements** (delete — App Group no longer needed) |
| 130 | + |
| 131 | +## Key Insights About the Repo |
| 132 | + |
| 133 | +### No .xcodeproj in Repo |
| 134 | +The `ios/` directory does NOT contain a `.xcodeproj` file. The Xcode project is managed |
| 135 | +separately. This means: |
| 136 | +- We cannot edit target membership via pbxproj |
| 137 | +- All changes are to source files only |
| 138 | +- The spec says "readability.js is bundled as a resource in the app target" — this is a |
| 139 | + Xcode project setting that must be done manually in Xcode. We can only place the file |
| 140 | + in the right directory. |
| 141 | +- Tests reference `@testable import Piper` and `@testable import PiperShareExtension` — |
| 142 | + these target names are defined in the Xcode project |
| 143 | + |
| 144 | +### CookieStorage Protocol Design |
| 145 | +The `CookieStorage` protocol in CookieManager.swift is excellent for testability. The only |
| 146 | +change needed is replacing `AppGroupStorage` (which uses `UserDefaults(suiteName:)`) with |
| 147 | +a new `StandardStorage` class that uses `UserDefaults.standard`. The `CookieManager` class |
| 148 | +itself stays unchanged. |
| 149 | + |
| 150 | +### ShareViewController Orchestration Logic → PipelineController |
| 151 | +`ShareViewController` orchestrates: cookieManager.hasCookies → extractSharedURL → |
| 152 | +contentExtractor.extract → apiClient.save → pasteboard.string = url → showSuccess. |
| 153 | + |
| 154 | +`PipelineController` should mirror this logic but: |
| 155 | +- Take a URL string as input (from UIPasteboard in the app) instead of NSExtensionContext |
| 156 | +- Be fully testable with injected mocks |
| 157 | +- Return results via completion handlers or async/await |
| 158 | + |
| 159 | +### UIPasteboardProtocol |
| 160 | +The `UIPasteboardProtocol` is currently defined in `ShareViewController.swift`. When that |
| 161 | +file is deleted, this protocol needs a new home. It could go in: |
| 162 | +- `ios/Piper/Services/PipelineController.swift` (since the controller reads clipboard) |
| 163 | +- Or a new small file |
| 164 | + |
| 165 | +Actually, looking more carefully at the spec: PipelineController takes a URL string as |
| 166 | +input (the view reads from clipboard and passes it). So the view is responsible for reading |
| 167 | +the clipboard, PipelineController just processes the URL. UIPasteboardProtocol may not be |
| 168 | +needed in PipelineController at all — the view reads UIPasteboard.general.string and passes |
| 169 | +it to PipelineController. |
| 170 | + |
| 171 | +### ContentExtractor Bundle |
| 172 | +`ContentExtractor` uses `Bundle(for: ContentExtractor.self)` as the default bundle. When |
| 173 | +moved from PiperShareExtension to Piper, this will automatically resolve to the Piper app |
| 174 | +bundle, which is correct. The error message "missing from the extension bundle" needs |
| 175 | +updating to "missing from the app bundle". |
| 176 | + |
| 177 | +### Config.swift Location |
| 178 | +`Config.swift` is in `ios/Shared/` (not in PiperShareExtension). The spec says "Backend |
| 179 | +URL is a single constant via Config.swift" — this is already the case. No change needed |
| 180 | +here except ensuring the file stays in Shared/ and is accessible to the Piper target. |
| 181 | + |
| 182 | +### Test Target Names |
| 183 | +- `PiperTests` imports `@testable import Piper` — these tests compile against the Piper target |
| 184 | +- `PiperShareExtensionTests` imports `@testable import PiperShareExtension` — must be updated |
| 185 | + to `@testable import Piper` when moved |
| 186 | + |
| 187 | +### InMemoryStorage Duplication |
| 188 | +`InMemoryStorage` is defined in both: |
| 189 | +- `ios/PiperTests/CookieManagerTests.swift` (as `final class InMemoryStorage: CookieStorage`) |
| 190 | +- `ios/PiperShareExtensionTests/ShareViewControllerTests.swift` (private version) |
| 191 | + |
| 192 | +The `LoginDetectionTests.swift` and `ContentViewTests.swift` reference `InMemoryStorage` |
| 193 | +without defining it — they rely on the definition in `CookieManagerTests.swift` being |
| 194 | +compiled in the same test target. The moved tests (ContentExtractorTests, etc.) that come |
| 195 | +from PiperShareExtension do NOT use InMemoryStorage, so no conflict there. |
| 196 | + |
| 197 | +## Potential Issues and Gotchas |
| 198 | + |
| 199 | +1. **No .xcodeproj in repo**: Cannot modify target membership. The spec's "Files to |
| 200 | + create" and "Files to move" are file-system operations only. The developer must manually |
| 201 | + update Xcode project settings (add files to targets, add readability.js to Copy Bundle |
| 202 | + Resources). The SETUP.md will need updating to reflect the new single-target setup. |
| 203 | + |
| 204 | +2. **readability.js bundle lookup**: After the move, `ContentExtractor` looks for |
| 205 | + `readability.js` via `bundle.url(forResource: "readability", withExtension: "js")`. |
| 206 | + This will work IF Xcode adds readability.js to the Piper target's "Copy Bundle Resources" |
| 207 | + phase. The file must be at `ios/Piper/Resources/readability.js` on disk. |
| 208 | + |
| 209 | +3. **PiperApp.swift currently passes CookieManager() to ContentView**. After the pivot, |
| 210 | + ContentView also needs a PipelineController. The PiperApp.swift will need to create a |
| 211 | + PipelineController and pass it to ContentView (or ContentView can create it internally). |
| 212 | + |
| 213 | +4. **ShareViewControllerTests.swift tests the UIKit ShareViewController** — this entire |
| 214 | + file is deleted (not moved). Coverage is replaced by PipelineControllerTests.swift which |
| 215 | + tests the equivalent orchestration logic at the service level. |
| 216 | + |
| 217 | +5. **UIPasteboardProtocol** is defined in ShareViewController.swift. When that file is |
| 218 | + deleted, the protocol disappears. If PipelineController tests need to mock clipboard |
| 219 | + writing, UIPasteboardProtocol needs to be re-homed. However, per the spec, the clipboard |
| 220 | + WRITE (copying result URL) could happen either in PipelineController or in the view. |
| 221 | + For testability, PipelineController should just return the URL string, and the view |
| 222 | + copies it to the clipboard. This avoids needing UIPasteboardProtocol in the service layer. |
| 223 | + |
| 224 | +6. **AppGroupStorage fatalError**: The current `AppGroupStorage.init()` calls `fatalError` |
| 225 | + if the UserDefaults suite can't be opened. Replacing with `StandardStorage` (wrapping |
| 226 | + `UserDefaults.standard`) eliminates this crash risk entirely. |
| 227 | + |
| 228 | +7. **Piper.entitlements**: Currently in `ios/Piper/Piper.entitlements`. The spec says to |
| 229 | + delete it. Since there's no .xcodeproj to edit, we only delete the file. The developer |
| 230 | + must also remove the entitlements file reference from the Xcode project settings for |
| 231 | + the Piper target. |
| 232 | + |
| 233 | +8. **SETUP.md needs updating**: The existing SETUP.md describes the two-target setup with |
| 234 | + App Group. After the pivot, it should describe the single-target setup. However, the |
| 235 | + spec doesn't list SETUP.md as a file to modify — this may be out of scope or handled |
| 236 | + separately. |
| 237 | + |
| 238 | +## Build Order (Implementation Sequence) |
| 239 | + |
| 240 | +1. Modify CookieManager.swift (AppGroupStorage → StandardStorage) |
| 241 | +2. Create ios/Piper/Services/ directory structure |
| 242 | +3. Copy ContentExtractor.swift to ios/Piper/Services/ (update bundle error message) |
| 243 | +4. Copy PiperAPIClient.swift to ios/Piper/Services/ |
| 244 | +5. Create ios/Piper/Resources/ directory and move readability.js |
| 245 | +6. Create ios/Piper/Services/PipelineController.swift |
| 246 | +7. Create ios/Piper/PipeView.swift |
| 247 | +8. Modify ios/Piper/ContentView.swift (add Pipe Article button + PipeView sheet) |
| 248 | +9. Move PiperShareExtensionTests/* → PiperTests/ (updating imports) |
| 249 | +10. Create ios/PiperTests/PipelineControllerTests.swift |
| 250 | +11. Update CookieManagerTests.swift (add StandardStorage test) |
| 251 | +12. Delete PiperShareExtension/ and PiperShareExtensionTests/ directories |
| 252 | +13. Delete ios/Piper/Piper.entitlements |
| 253 | +14. Update SETUP.md if required |
0 commit comments