general: appstore build stuff
This commit is contained in:
10
.distribution_configs/appstore/Gaze.entitlements
Normal file
10
.distribution_configs/appstore/Gaze.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
26
.distribution_configs/appstore/Info.plist
Normal file
26
.distribution_configs/appstore/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
36
.distribution_configs/appstore/project_release.txt
Normal file
36
.distribution_configs/appstore/project_release.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Gaze/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
15
.distribution_configs/self/Gaze.entitlements
Normal file
15
.distribution_configs/self/Gaze.entitlements
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
36
.distribution_configs/self/Info.plist
Normal file
36
.distribution_configs/self/Info.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://freno.me/api/Gaze/appcast.xml</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>SUScheduledCheckInterval</key>
|
||||
<integer>86400</integer>
|
||||
<key>SUEnableInstallerLauncherService</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
35
.distribution_configs/self/project_release.txt
Normal file
35
.distribution_configs/self/project_release.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Gaze/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
214
DISTRIBUTION.md
Normal file
214
DISTRIBUTION.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Gaze Distribution Guide
|
||||
|
||||
This guide explains how to build and distribute Gaze for both the Mac App Store and direct distribution with auto-updates.
|
||||
|
||||
## Distribution Methods
|
||||
|
||||
Gaze supports two distribution methods:
|
||||
|
||||
1. **Self-Distribution** (Direct Download) - Includes Sparkle for automatic updates
|
||||
2. **Mac App Store** - Uses Apple's update mechanism, no Sparkle
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Switching Between Distributions
|
||||
|
||||
Use the `switch_to` script to configure the project for each distribution method:
|
||||
|
||||
```bash
|
||||
# For self-distribution with Sparkle auto-updates
|
||||
./switch_to self
|
||||
|
||||
# For Mac App Store submission
|
||||
./switch_to appstore
|
||||
|
||||
# Check current configuration
|
||||
./switch_to status
|
||||
```
|
||||
|
||||
### What Gets Changed
|
||||
|
||||
The `switch_to` script automatically manages:
|
||||
|
||||
**Self-Distribution Mode:**
|
||||
- ✅ Adds Sparkle keys to `Info.plist` (SUPublicEDKey, SUFeedURL, etc.)
|
||||
- ✅ Adds Sparkle entitlements for XPC services
|
||||
- ✅ Removes `APPSTORE` compiler flag
|
||||
- ✅ Enables UpdateManager with Sparkle framework
|
||||
|
||||
**App Store Mode:**
|
||||
- ✅ Removes all Sparkle keys from `Info.plist`
|
||||
- ✅ Removes Sparkle entitlements
|
||||
- ✅ Adds `-D APPSTORE` compiler flag
|
||||
- ✅ Disables Sparkle code at compile time
|
||||
|
||||
## Building for Self-Distribution
|
||||
|
||||
```bash
|
||||
# 1. Switch to self-distribution mode
|
||||
./switch_to self
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Prompt for version bump (major/minor/patch)
|
||||
- Build and code sign with Developer ID
|
||||
- Notarize the app with Apple
|
||||
- Create a signed DMG
|
||||
- Generate Sparkle appcast with EdDSA signature
|
||||
- (Optional) Upload to S3 if credentials are configured
|
||||
|
||||
## Building for Mac App Store
|
||||
|
||||
```bash
|
||||
# 1. Switch to App Store mode
|
||||
./switch_to appstore
|
||||
|
||||
# 2. Add Run Script Phase in Xcode (one-time setup)
|
||||
# See section below
|
||||
|
||||
# 3. Archive and distribute via Xcode
|
||||
# Product → Archive
|
||||
# Window → Organizer → Distribute App → App Store Connect
|
||||
```
|
||||
|
||||
### Required: Run Script Phase
|
||||
|
||||
For App Store builds, you **must** add this Run Script phase in Xcode:
|
||||
|
||||
1. Open Gaze.xcodeproj in Xcode
|
||||
2. Select the Gaze target → Build Phases
|
||||
3. Click + → New Run Script Phase
|
||||
4. Name it: "Remove Sparkle for App Store"
|
||||
5. Place it **after** "Embed Frameworks"
|
||||
6. Add this script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then
|
||||
echo "Removing Sparkle framework for App Store build..."
|
||||
rm -rf "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework"
|
||||
echo "Sparkle framework removed successfully"
|
||||
fi
|
||||
```
|
||||
|
||||
This ensures Sparkle.framework is removed from the app bundle before submission.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Configuration backups are stored in `.distribution_configs/`:
|
||||
- `.distribution_configs/appstore/` - App Store configuration
|
||||
- `.distribution_configs/self/` - Self-distribution configuration
|
||||
|
||||
These backups are created automatically and used when switching between modes.
|
||||
|
||||
## Validation
|
||||
|
||||
Before submitting to App Store Connect, verify your configuration:
|
||||
|
||||
```bash
|
||||
./switch_to status
|
||||
```
|
||||
|
||||
Expected output for App Store:
|
||||
```
|
||||
Info.plist: App Store (no Sparkle keys)
|
||||
Entitlements: App Store (no Sparkle exceptions)
|
||||
Build Settings: App Store (has APPSTORE flag)
|
||||
```
|
||||
|
||||
Expected output for Self-Distribution:
|
||||
```
|
||||
Info.plist: Self-Distribution (has Sparkle keys)
|
||||
Entitlements: Self-Distribution (has Sparkle exceptions)
|
||||
Build Settings: Self-Distribution (no APPSTORE flag)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App Store Validation Fails
|
||||
|
||||
**Error: "App sandbox not enabled" with Sparkle executables**
|
||||
- Solution: Make sure you ran `./switch_to appstore` and added the Run Script phase
|
||||
|
||||
**Error: "Bad Bundle Executable" or "CFBundlePackageType"**
|
||||
- Solution: These are now fixed in the Info.plist
|
||||
|
||||
**Error: Still seeing Sparkle in the build**
|
||||
- Solution: Clean build folder (⌘⇧K) and rebuild
|
||||
|
||||
### Self-Distribution Issues
|
||||
|
||||
**Sparkle updates not working**
|
||||
- Verify: `./switch_to status` shows "Self-Distribution" mode
|
||||
- Check: Info.plist contains SUPublicEDKey and SUFeedURL
|
||||
- Verify: Appcast is accessible at the SUFeedURL
|
||||
|
||||
**Code signing issues**
|
||||
- Check `.env` file has correct credentials
|
||||
- Verify Developer ID certificate: `security find-identity -v -p codesigning`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
For self-distribution, create a `.env` file with:
|
||||
|
||||
```bash
|
||||
# Required for code signing
|
||||
DEVELOPER_ID_APPLICATION="Developer ID Application: Your Name (TEAM_ID)"
|
||||
APPLE_TEAM_ID="XXXXXXXXXX"
|
||||
|
||||
# Required for notarization
|
||||
NOTARY_KEYCHAIN_PROFILE="notary-profile"
|
||||
|
||||
# Optional for S3 upload
|
||||
AWS_ACCESS_KEY_ID="your-key"
|
||||
AWS_SECRET_ACCESS_KEY="your-secret"
|
||||
AWS_BUCKET_NAME="your-bucket"
|
||||
AWS_REGION="us-east-1"
|
||||
```
|
||||
|
||||
Setup notarization profile (one-time):
|
||||
```bash
|
||||
xcrun notarytool store-credentials "notary-profile" \
|
||||
--apple-id "your@email.com" \
|
||||
--team-id "TEAM_ID"
|
||||
```
|
||||
|
||||
## Version Management
|
||||
|
||||
The `self_distribute` script handles version bumping:
|
||||
- **Major** (X.0.0) - Breaking changes
|
||||
- **Minor** (x.X.0) - New features
|
||||
- **Patch** (x.x.X) - Bug fixes
|
||||
- **Custom** - Any version string
|
||||
- **Keep** - Increment build number only
|
||||
|
||||
Git tags are created automatically for new versions.
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Self-Distribution Build
|
||||
```bash
|
||||
./switch_to self
|
||||
# Test the DMG on a clean macOS system
|
||||
```
|
||||
|
||||
### Test App Store Build
|
||||
```bash
|
||||
./switch_to appstore
|
||||
# Archive in Xcode
|
||||
# Use TestFlight for testing before release
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use `switch_to`** - Don't manually edit configuration files
|
||||
2. **Check status before building** - Use `./switch_to status`
|
||||
3. **Clean builds** - Run Clean Build Folder when switching modes
|
||||
4. **Test thoroughly** - Test both distribution methods separately
|
||||
5. **Commit before switching** - Use git to track configuration changes
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: https://github.com/mikefreno/Gaze/issues
|
||||
- Check AGENTS.md for development guidelines
|
||||
@@ -121,6 +121,7 @@
|
||||
buildPhases = (
|
||||
27A21B382F0F69DC0018C4F3 /* Sources */,
|
||||
27A21B392F0F69DC0018C4F3 /* Frameworks */,
|
||||
27D081082F16AA7100FF3A31 /* Run Script */,
|
||||
27A21B3A2F0F69DC0018C4F3 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
@@ -257,6 +258,27 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
27D081082F16AA7100FF3A31 /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = " #!/bin/bash\n if [[ \"${OTHER_SWIFT_FLAGS}\" == *\"APPSTORE\"* ]]; then\n echo \"Removing Sparkle framework for App Store build...\"\n rm -rf \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework\"\n echo \"Sparkle framework removed successfully\"\n fi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
27A21B382F0F69DC0018C4F3 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@@ -475,6 +497,7 @@
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
|
||||
0
Gaze.xcodeproj/project.pbxproj.tmp
Normal file
0
Gaze.xcodeproj/project.pbxproj.tmp
Normal file
@@ -193,8 +193,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||
window.backgroundColor = .clear
|
||||
window.contentView = NSHostingView(rootView: content)
|
||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
window.acceptsMouseMovedEvents = !requiresFocus
|
||||
window.ignoresMouseEvents = !requiresFocus
|
||||
|
||||
// Allow mouse events for all reminders (needed for dismiss button)
|
||||
window.acceptsMouseMovedEvents = true
|
||||
window.ignoresMouseEvents = false
|
||||
|
||||
let windowController = NSWindowController(window: window)
|
||||
windowController.showWindow(nil)
|
||||
|
||||
@@ -6,10 +6,5 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -12,6 +12,15 @@ struct GazeApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject private var settingsManager = SettingsManager.shared
|
||||
|
||||
init() {
|
||||
// Handle test launch arguments
|
||||
if TestingEnvironment.shouldSkipOnboarding {
|
||||
SettingsManager.shared.settings.hasCompletedOnboarding = true
|
||||
} else if TestingEnvironment.shouldResetOnboarding {
|
||||
SettingsManager.shared.settings.hasCompletedOnboarding = false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
// Onboarding window (only shown when not completed)
|
||||
WindowGroup {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
@@ -18,15 +22,5 @@
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://freno.me/api/Gaze/appcast.xml</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>SUScheduledCheckInterval</key>
|
||||
<integer>86400</integer>
|
||||
<key>SUEnableInstallerLauncherService</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -7,24 +7,31 @@
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
#if !APPSTORE
|
||||
import Sparkle
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
class UpdateManager: NSObject, ObservableObject {
|
||||
static let shared = UpdateManager()
|
||||
|
||||
#if !APPSTORE
|
||||
private var updaterController: SPUStandardUpdaterController?
|
||||
private var automaticallyChecksObservation: NSKeyValueObservation?
|
||||
private var lastCheckDateObservation: NSKeyValueObservation?
|
||||
#endif
|
||||
|
||||
@Published var automaticallyChecksForUpdates = false
|
||||
@Published var lastUpdateCheckDate: Date?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
#if !APPSTORE
|
||||
setupUpdater()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !APPSTORE
|
||||
private func setupUpdater() {
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
startingUpdater: true,
|
||||
@@ -57,17 +64,24 @@ class UpdateManager: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
func checkForUpdates() {
|
||||
#if !APPSTORE
|
||||
guard let updater = updaterController?.updater else {
|
||||
print("Updater not initialized")
|
||||
return
|
||||
}
|
||||
updater.checkForUpdates()
|
||||
#else
|
||||
print("Updates are managed by the App Store")
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if !APPSTORE
|
||||
automaticallyChecksObservation?.invalidate()
|
||||
lastCheckDateObservation?.invalidate()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +101,15 @@ struct LookAwayReminderView: View {
|
||||
}
|
||||
|
||||
private func startCountdown() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
let timer = Timer(timeInterval: 1.0, repeats: true) { [self] _ in
|
||||
if remainingSeconds > 0 {
|
||||
remainingSeconds -= 1
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
RunLoop.current.add(timer, forMode: .common)
|
||||
self.timer = timer
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
|
||||
@@ -101,13 +101,15 @@ struct UserTimerOverlayReminderView: View {
|
||||
}
|
||||
|
||||
private func startCountdown() {
|
||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
let timer = Timer(timeInterval: 1.0, repeats: true) { [self] _ in
|
||||
if remainingSeconds > 0 {
|
||||
remainingSeconds -= 1
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
RunLoop.current.add(timer, forMode: .common)
|
||||
countdownTimer = timer
|
||||
}
|
||||
|
||||
private func dismiss() {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Ensure we're using self-distribution configuration
|
||||
echo "🔄 Switching to self-distribution configuration..."
|
||||
./switch_to self
|
||||
|
||||
# Load environment variables from .env file
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
|
||||
348
switch_to
Executable file
348
switch_to
Executable file
@@ -0,0 +1,348 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration paths
|
||||
INFO_PLIST="Gaze/Info.plist"
|
||||
ENTITLEMENTS="Gaze/Gaze.entitlements"
|
||||
PROJECT_FILE="Gaze.xcodeproj/project.pbxproj"
|
||||
BACKUP_DIR=".distribution_configs"
|
||||
|
||||
# Distribution configurations
|
||||
APPSTORE_CONFIG="${BACKUP_DIR}/appstore"
|
||||
SELF_CONFIG="${BACKUP_DIR}/self"
|
||||
|
||||
# Function to print colored messages
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
# Function to create backup directories
|
||||
create_backup_dirs() {
|
||||
mkdir -p "${APPSTORE_CONFIG}"
|
||||
mkdir -p "${SELF_CONFIG}"
|
||||
}
|
||||
|
||||
# Function to backup current configuration
|
||||
backup_current_config() {
|
||||
local config_name=$1
|
||||
local config_dir=$2
|
||||
|
||||
print_info "Backing up ${config_name} configuration..."
|
||||
|
||||
# Backup Info.plist
|
||||
if [ -f "${INFO_PLIST}" ]; then
|
||||
cp "${INFO_PLIST}" "${config_dir}/Info.plist"
|
||||
fi
|
||||
|
||||
# Backup entitlements
|
||||
if [ -f "${ENTITLEMENTS}" ]; then
|
||||
cp "${ENTITLEMENTS}" "${config_dir}/Gaze.entitlements"
|
||||
fi
|
||||
|
||||
# Backup relevant parts of project.pbxproj (just the Release config)
|
||||
if [ -f "${PROJECT_FILE}" ]; then
|
||||
# Extract the Release configuration section
|
||||
awk '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/ {print}' "${PROJECT_FILE}" > "${config_dir}/project_release.txt"
|
||||
fi
|
||||
|
||||
print_success "Backed up ${config_name} configuration"
|
||||
}
|
||||
|
||||
# Function to restore configuration
|
||||
restore_config() {
|
||||
local config_name=$1
|
||||
local config_dir=$2
|
||||
|
||||
print_info "Restoring ${config_name} configuration..."
|
||||
|
||||
# Restore Info.plist
|
||||
if [ -f "${config_dir}/Info.plist" ]; then
|
||||
cp "${config_dir}/Info.plist" "${INFO_PLIST}"
|
||||
print_success "Restored Info.plist"
|
||||
else
|
||||
print_warning "No Info.plist backup found for ${config_name}"
|
||||
fi
|
||||
|
||||
# Restore entitlements
|
||||
if [ -f "${config_dir}/Gaze.entitlements" ]; then
|
||||
cp "${config_dir}/Gaze.entitlements" "${ENTITLEMENTS}"
|
||||
print_success "Restored entitlements"
|
||||
else
|
||||
print_warning "No entitlements backup found for ${config_name}"
|
||||
fi
|
||||
|
||||
# Restore project.pbxproj Release configuration
|
||||
if [ -f "${config_dir}/project_release.txt" ]; then
|
||||
# Check if we're restoring to appstore or self mode based on file content
|
||||
if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${config_dir}/project_release.txt"; then
|
||||
# Add APPSTORE flag
|
||||
if ! grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}"; then
|
||||
# Find the Release config section and add the flag
|
||||
sed -i.backup '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/{
|
||||
/MARKETING_VERSION = /a\
|
||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
||||
}' "${PROJECT_FILE}"
|
||||
rm -f "${PROJECT_FILE}.backup"
|
||||
print_success "Added APPSTORE compiler flag"
|
||||
fi
|
||||
else
|
||||
# Remove APPSTORE flag
|
||||
if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}"; then
|
||||
sed -i.backup '/OTHER_SWIFT_FLAGS = "-D APPSTORE";/d' "${PROJECT_FILE}"
|
||||
rm -f "${PROJECT_FILE}.backup"
|
||||
print_success "Removed APPSTORE compiler flag"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_warning "No project.pbxproj backup found for ${config_name}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to initialize configurations if they don't exist
|
||||
initialize_configs() {
|
||||
create_backup_dirs
|
||||
|
||||
# Check if we have existing backups
|
||||
if [ ! -f "${SELF_CONFIG}/Info.plist" ]; then
|
||||
print_info "No self-distribution config found. Creating from current state..."
|
||||
|
||||
# The current state should be self-distribution (before my changes)
|
||||
# Let's create the self-distribution version with Sparkle keys
|
||||
cat > "${SELF_CONFIG}/Info.plist" <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Gaze</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>Z2RmohI1y2bgeGQQUDqO9F0HNF2AzFotOt8CwGB6VJM=</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://freno.me/api/Gaze/appcast.xml</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>SUScheduledCheckInterval</key>
|
||||
<integer>86400</integer>
|
||||
<key>SUEnableInstallerLauncherService</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
cat > "${SELF_CONFIG}/Gaze.entitlements" <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Extract current Release config WITHOUT APPSTORE flag
|
||||
awk '/27A21B5F2F0F69DD0018C4F3 \/\* Release \*\/ = {/,/name = Release;/ {
|
||||
if ($0 !~ /OTHER_SWIFT_FLAGS/) {
|
||||
print
|
||||
} else {
|
||||
# Skip the APPSTORE flag line
|
||||
if ($0 !~ /APPSTORE/) {
|
||||
print
|
||||
}
|
||||
}
|
||||
}' "${PROJECT_FILE}" > "${SELF_CONFIG}/project_release.txt"
|
||||
|
||||
print_success "Created self-distribution config"
|
||||
fi
|
||||
|
||||
if [ ! -f "${APPSTORE_CONFIG}/Info.plist" ]; then
|
||||
print_info "Creating App Store config..."
|
||||
|
||||
# Backup current state as App Store config (after my changes)
|
||||
backup_current_config "App Store" "${APPSTORE_CONFIG}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show current distribution mode
|
||||
show_current_mode() {
|
||||
print_info "Current configuration:"
|
||||
echo ""
|
||||
|
||||
# Check for Sparkle keys in Info.plist
|
||||
if grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then
|
||||
echo " Info.plist: ${GREEN}Self-Distribution${NC} (has Sparkle keys)"
|
||||
else
|
||||
echo " Info.plist: ${BLUE}App Store${NC} (no Sparkle keys)"
|
||||
fi
|
||||
|
||||
# Check for Sparkle entitlements
|
||||
if grep -q "mach-lookup.global-name" "${ENTITLEMENTS}" 2>/dev/null; then
|
||||
echo " Entitlements: ${GREEN}Self-Distribution${NC} (has Sparkle exceptions)"
|
||||
else
|
||||
echo " Entitlements: ${BLUE}App Store${NC} (no Sparkle exceptions)"
|
||||
fi
|
||||
|
||||
# Check for APPSTORE flag
|
||||
if grep -q "OTHER_SWIFT_FLAGS.*APPSTORE" "${PROJECT_FILE}" 2>/dev/null; then
|
||||
echo " Build Settings: ${BLUE}App Store${NC} (has APPSTORE flag)"
|
||||
else
|
||||
echo " Build Settings: ${GREEN}Self-Distribution${NC} (no APPSTORE flag)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to switch to App Store configuration
|
||||
switch_to_appstore() {
|
||||
print_info "Switching to App Store distribution configuration..."
|
||||
echo ""
|
||||
|
||||
# Backup current state if it's self-distribution
|
||||
if grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then
|
||||
backup_current_config "self-distribution" "${SELF_CONFIG}"
|
||||
fi
|
||||
|
||||
# Restore App Store config
|
||||
restore_config "App Store" "${APPSTORE_CONFIG}"
|
||||
|
||||
echo ""
|
||||
print_success "Switched to App Store distribution mode"
|
||||
print_warning "Remember to add the Run Script phase to remove Sparkle framework!"
|
||||
echo ""
|
||||
echo "Run Script to add in Xcode Build Phases:"
|
||||
echo '#!/bin/bash'
|
||||
echo 'if [[ "${OTHER_SWIFT_FLAGS}" == *"APPSTORE"* ]]; then'
|
||||
echo ' rm -rf "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework"'
|
||||
echo 'fi'
|
||||
}
|
||||
|
||||
# Function to switch to self-distribution configuration
|
||||
switch_to_self() {
|
||||
print_info "Switching to self-distribution configuration..."
|
||||
echo ""
|
||||
|
||||
# Backup current state if it's App Store
|
||||
if ! grep -q "SUPublicEDKey" "${INFO_PLIST}" 2>/dev/null; then
|
||||
backup_current_config "App Store" "${APPSTORE_CONFIG}"
|
||||
fi
|
||||
|
||||
# Restore self-distribution config
|
||||
restore_config "self-distribution" "${SELF_CONFIG}"
|
||||
|
||||
echo ""
|
||||
print_success "Switched to self-distribution mode"
|
||||
print_info "Sparkle auto-updates enabled"
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
cat << EOF
|
||||
${BLUE}Gaze Distribution Configuration Switcher${NC}
|
||||
|
||||
${GREEN}Usage:${NC}
|
||||
./switch_to [command]
|
||||
|
||||
${GREEN}Commands:${NC}
|
||||
appstore Switch to App Store distribution configuration
|
||||
- Removes Sparkle keys from Info.plist
|
||||
- Removes Sparkle entitlements
|
||||
- Adds APPSTORE compiler flag
|
||||
|
||||
self Switch to self-distribution configuration
|
||||
- Adds Sparkle keys to Info.plist
|
||||
- Adds Sparkle entitlements for XPC services
|
||||
- Removes APPSTORE compiler flag
|
||||
|
||||
status Show current distribution configuration
|
||||
|
||||
help Show this help message
|
||||
|
||||
${GREEN}Examples:${NC}
|
||||
./switch_to appstore # Prepare for App Store submission
|
||||
./switch_to self # Prepare for direct distribution with auto-updates
|
||||
./switch_to status # Check current configuration
|
||||
|
||||
${YELLOW}Note:${NC} Configuration backups are stored in ${BACKUP_DIR}/
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
main() {
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "${PROJECT_FILE}" ]; then
|
||||
print_error "Not in Gaze project directory. Please run from project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Initialize configurations if needed
|
||||
initialize_configs
|
||||
|
||||
# Parse command
|
||||
case "${1:-}" in
|
||||
appstore)
|
||||
switch_to_appstore
|
||||
;;
|
||||
self)
|
||||
switch_to_self
|
||||
;;
|
||||
status)
|
||||
show_current_mode
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown command: ${1:-}"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user