diff --git a/ARCHIVE_POST_ACTION_SETUP.md b/ARCHIVE_POST_ACTION_SETUP.md new file mode 100644 index 0000000..4cd6e62 --- /dev/null +++ b/ARCHIVE_POST_ACTION_SETUP.md @@ -0,0 +1,201 @@ +# Archive Post-Action Setup (REQUIRED for App Store) + +## The Problem + +App Store validation fails with: +``` +App sandbox not enabled. The following executables must include the +"com.apple.security.app-sandbox" entitlement... +Sparkle.framework/Versions/B/Autoupdate +Sparkle.framework/Versions/B/Updater.app +... +``` + +Sparkle.framework **must be physically removed** from the app bundle for App Store distribution. + +## Why Build Phase Scripts Don't Work + +Build Phase scripts run during compilation when files are code-signed and locked by macOS. Even with `chmod` and `chflags`, you get "Operation not permitted" due to System Integrity Protection. + +## The Solution: Archive Post-Action + +Archive Post-Actions run **after** the archive completes, when files are no longer locked. This is the correct place to remove Sparkle. + +--- + +## Setup Instructions (2 minutes) + +### 1. Open Scheme Editor +In Xcode: **Product → Scheme → Edit Scheme...** (or press **⌘<**) + +### 2. Select Archive +Click **Archive** in the left sidebar + +### 3. Add Post-Action Script +- At the bottom, under "Post-actions", click the **+** button +- Select **New Run Script Action** + +### 4. Configure the Action + +**Provide build settings from:** Select **Gaze** from the dropdown (IMPORTANT!) + +**Shell:** Leave as `/bin/bash` + +**Script:** Copy and paste this entire script: + +```bash +if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "🗑️ Removing Sparkle from archived app..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + SPARKLE_PATH="${ARCHIVE_PATH}/Products/Applications/Gaze.app/Contents/Frameworks/Sparkle.framework" + + if [ -d "$SPARKLE_PATH" ]; then + echo "📂 Found Sparkle at: $SPARKLE_PATH" + + # Make writable and remove + chmod -R u+w "$SPARKLE_PATH" 2>/dev/null || true + chflags -R nouchg "$SPARKLE_PATH" 2>/dev/null || true + rm -rf "$SPARKLE_PATH" + + if [ ! -d "$SPARKLE_PATH" ]; then + echo "✅ Sparkle framework removed successfully!" + else + echo "❌ ERROR: Could not remove Sparkle framework" + echo " This will cause App Store validation to fail" + exit 1 + fi + else + echo "ℹ️ Sparkle framework not found (already removed)" + fi + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "✅ Archive ready for App Store distribution" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +else + echo "✓ Self-distribution archive - Sparkle retained" +fi +``` + +### 5. Save +Click **Close** to save the scheme changes + +--- + +## Verification Steps + +### Test the Archive + +1. **Switch to App Store mode:** + ```bash + ./switch_to appstore + ``` + +2. **Archive in Xcode:** + - **Product → Archive** (or **⌘⇧B** then Archive) + - Watch the build log - you should see the post-action output + +3. **Check the archive contents:** + - **Window → Organizer** + - Select your latest Gaze archive + - Right-click → **Show in Finder** + - Right-click the `.xcarchive` file → **Show Package Contents** + - Navigate to: `Products/Applications/Gaze.app/Contents/Frameworks/` + - **Verify:** Only `Lottie.framework` should be present ✅ + +4. **Distribute to App Store:** + - In Organizer, click **Distribute App** + - Choose **App Store Connect** + - Complete the distribution wizard + - **Validation should now pass!** ✅ + +--- + +## What You Should See + +### In the Build Log (after archiving): +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🗑️ Removing Sparkle from archived app... +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📂 Found Sparkle at: [path] +✅ Sparkle framework removed successfully! +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✅ Archive ready for App Store distribution +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### In the Frameworks folder: +``` +Gaze.app/Contents/Frameworks/ +└── Lottie.framework/ +``` + +No Sparkle.framework! ✅ + +--- + +## Troubleshooting + +### "I don't see the post-action output" +- Make sure you selected **Gaze** in "Provide build settings from" +- Check View → Navigators → Show Report Navigator (⌘9) +- Select the Archive action to see full logs + +### "Sparkle is still in the archive" +- Verify `./switch_to status` shows "App Store" for all items +- Check the script exactly matches (copy/paste the entire script) +- Try cleaning: Product → Clean Build Folder (⌘⇧K) + +### "Script says 'Sparkle framework not found'" +- This means Sparkle wasn't embedded (good!) +- Continue with distribution - validation should pass + +### "Archive Post-Action section doesn't exist" +- Make sure you're editing the **Archive** section, not Run or Test +- Click the triangle next to "Archive" to expand it + +--- + +## Optional: Remove Old Build Phase Script + +If you previously added a Build Phase script (which doesn't work due to file locking), you can remove it: + +1. Gaze target → Build Phases +2. Find "Remove Sparkle for App Store" or "Run Script" +3. Click the **X** to delete it + +The Archive Post-Action is the correct and only solution needed. + +--- + +## Why This Is Required + +Even though: +- ✅ Sparkle code is disabled via `#if !APPSTORE` +- ✅ Info.plist has no Sparkle keys +- ✅ Entitlements have no Sparkle exceptions + +App Store validation **still checks** for the physical presence of unsandboxed executables in frameworks. Sparkle contains XPC services that aren't App Store compatible, so the entire framework must be removed. + +--- + +## For Self-Distribution + +When building for self-distribution (`./switch_to self`), the script detects the absence of the APPSTORE flag and leaves Sparkle intact. You don't need to change anything! + +```bash +./switch_to self +./self_distribute # Sparkle is retained and works normally +``` + +--- + +## Summary + +✅ **One-time setup:** Add Archive Post-Action script +✅ **Works automatically:** Removes Sparkle only for App Store builds +✅ **Zero maintenance:** Once configured, runs automatically forever + +This is the **correct and only working solution** for removing Sparkle from App Store builds! diff --git a/Gaze.xcodeproj/project.pbxproj b/Gaze.xcodeproj/project.pbxproj index 01cecfc..2ba1ab2 100644 --- a/Gaze.xcodeproj/project.pbxproj +++ b/Gaze.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; }; - 275915902F132B0000D0E60D /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B22F10B20000E00DBC /* Sparkle */; }; + 275915902F132B0000D0E60D /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B22F10B20000E00DBC /* Sparkle */; settings = {ATTRIBUTES = (Weak, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ diff --git a/Gaze.xcodeproj/xcshareddata/xcschemes/Gaze.xcscheme b/Gaze.xcodeproj/xcshareddata/xcschemes/Gaze.xcscheme new file mode 100644 index 0000000..e7dc879 --- /dev/null +++ b/Gaze.xcodeproj/xcshareddata/xcschemes/Gaze.xcscheme @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gaze.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist b/Gaze.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist index e4347a5..236396b 100644 --- a/Gaze.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Gaze.xcodeproj/xcuserdata/mike.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,23 @@ 0 + SuppressBuildableAutocreation + + 27A21B3B2F0F69DC0018C4F3 + + primary + + + 27A21B482F0F69DD0018C4F3 + + primary + + + 27A21B522F0F69DD0018C4F3 + + primary + + + diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 21e1f14..b2ef003 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -20,28 +20,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { private var hasStartedTimers = false func applicationDidFinishLaunching(_ notification: Notification) { + print("🚀 Gaze: applicationDidFinishLaunching") + // Set activation policy to hide dock icon NSApplication.shared.setActivationPolicy(.accessory) + print("✓ Activation policy set to accessory") timerEngine = TimerEngine(settingsManager: settingsManager) + print("✓ TimerEngine initialized") // Initialize update manager after onboarding is complete if settingsManager.settings.hasCompletedOnboarding { + print("✓ Onboarding completed, initializing UpdateManager") updateManager = UpdateManager.shared + } else { + print("ℹ️ Onboarding not completed, skipping UpdateManager") } // Detect App Store version asynchronously at launch Task { - await settingsManager.detectAppStoreVersion() + do { + print("🔍 Detecting App Store version...") + await settingsManager.detectAppStoreVersion() + print("✓ App Store detection complete: \(settingsManager.settings.isAppStoreVersion)") + } catch { + print("⚠️ Failed to detect App Store version: \(error)") + } } setupLifecycleObservers() + print("✓ Lifecycle observers set up") + observeSettingsChanges() + print("✓ Settings change observer set up") // Start timers if onboarding is complete if settingsManager.settings.hasCompletedOnboarding { + print("▶️ Starting timers (onboarding complete)") startTimers() + } else { + print("⏸️ Timers not started (onboarding incomplete)") } + + print("✅ Gaze: Launch complete") } func onboardingCompleted() { diff --git a/Gaze/GazeApp.swift b/Gaze/GazeApp.swift index d8f7e37..d89c064 100644 --- a/Gaze/GazeApp.swift +++ b/Gaze/GazeApp.swift @@ -13,12 +13,18 @@ struct GazeApp: App { @StateObject private var settingsManager = SettingsManager.shared init() { + print("🚀 GazeApp: init") + // Handle test launch arguments if TestingEnvironment.shouldSkipOnboarding { + print("ℹ️ Test mode: Skipping onboarding") SettingsManager.shared.settings.hasCompletedOnboarding = true } else if TestingEnvironment.shouldResetOnboarding { + print("ℹ️ Test mode: Resetting onboarding") SettingsManager.shared.settings.hasCompletedOnboarding = false } + + print("✓ GazeApp initialized") } var body: some Scene { diff --git a/Gaze/Services/AppStoreDetector.swift b/Gaze/Services/AppStoreDetector.swift index 711c9ec..a6ced7c 100644 --- a/Gaze/Services/AppStoreDetector.swift +++ b/Gaze/Services/AppStoreDetector.swift @@ -17,30 +17,55 @@ enum AppStoreDetector { /// This method is asynchronous due to the use of StoreKit's async API. static func isAppStoreVersion() async -> Bool { #if DEBUG + print("🔍 AppStoreDetector: DEBUG build, returning false") return false #else + print("🔍 AppStoreDetector: Checking App Store status...") if #available(macOS 15.0, *) { + print(" ℹ️ Using macOS 15+ AppTransaction API") do { - _ = try await AppTransaction.shared + let transaction = try await AppTransaction.shared + print(" ✅ AppTransaction found: This is an App Store version") return true } catch { + print(" ⚠️ AppTransaction error: \(error.localizedDescription)") + print(" → Assuming NOT an App Store version") return false } } else { // Fallback for older macOS: use legacy receipt check + print(" ℹ️ Using legacy receipt check (macOS <15)") + guard let receiptURL = Bundle.main.appStoreReceiptURL else { + print(" ⚠️ No receipt URL available") return false } - guard FileManager.default.fileExists(atPath: receiptURL.path) else { + print(" 📄 Receipt URL: \(receiptURL.path)") + + do { + let fileExists = FileManager.default.fileExists(atPath: receiptURL.path) + guard fileExists else { + print(" ⚠️ Receipt file does not exist") + return false + } + print(" ✓ Receipt file exists") + + guard let receiptData = try? Data(contentsOf: receiptURL), + receiptData.count > 2 + else { + print(" ⚠️ Receipt file is empty or unreadable") + return false + } + print(" ✓ Receipt data loaded (\(receiptData.count) bytes)") + + let bytes = [UInt8](receiptData.prefix(2)) + let isValid = bytes[0] == 0x30 && bytes[1] == 0x82 + print(" \(isValid ? "✅" : "⚠️") Receipt validation: \(isValid)") + return isValid + } catch { + print(" ❌ Receipt check error: \(error.localizedDescription)") return false } - guard let receiptData = try? Data(contentsOf: receiptURL), - receiptData.count > 2 - else { - return false - } - let bytes = [UInt8](receiptData.prefix(2)) - return bytes[0] == 0x30 && bytes[1] == 0x82 } #endif }