general: removal of AppStoreDetector, just use build flags
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
||||||
|
27SPARKLE00000000003 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27SPARKLE00000000002 /* Sparkle */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
||||||
|
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -131,6 +133,7 @@
|
|||||||
name = Gaze;
|
name = Gaze;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||||
|
27SPARKLE00000000002 /* Sparkle */,
|
||||||
);
|
);
|
||||||
productName = Gaze;
|
productName = Gaze;
|
||||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||||
@@ -216,6 +219,7 @@
|
|||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||||
@@ -435,7 +439,6 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.4.0;
|
MARKETING_VERSION = 0.4.0;
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -472,7 +475,6 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.4.0;
|
MARKETING_VERSION = 0.4.0;
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -615,6 +617,14 @@
|
|||||||
minimumVersion = 4.6.0;
|
minimumVersion = 4.6.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||||
|
requirement = {
|
||||||
|
kind = exactVersion;
|
||||||
|
version = 2.8.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -623,6 +633,11 @@
|
|||||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
productName = Lottie;
|
productName = Lottie;
|
||||||
};
|
};
|
||||||
|
27SPARKLE00000000002 /* Sparkle */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
productName = Sparkle;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
||||||
|
|||||||
@@ -9,6 +9,15 @@
|
|||||||
"revision" : "69faaefa7721fba9e434a52c16adf4329c9084db",
|
"revision" : "69faaefa7721fba9e434a52c16adf4329c9084db",
|
||||||
"version" : "4.6.0"
|
"version" : "4.6.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sparkle",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
|
||||||
|
"version" : "2.8.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -31,15 +31,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
updateManager = UpdateManager.shared
|
updateManager = UpdateManager.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect App Store version asynchronously at launch
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
await settingsManager.detectAppStoreVersion()
|
|
||||||
} catch {
|
|
||||||
// Handle error silently in production
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupLifecycleObservers()
|
setupLifecycleObservers()
|
||||||
|
|
||||||
observeSettingsChanges()
|
observeSettingsChanges()
|
||||||
|
|||||||
@@ -6,5 +6,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -22,5 +22,15 @@
|
|||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>Copyright © 2026 Mike Freno. All rights reserved.</string>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
var launchAtLogin: Bool
|
var launchAtLogin: Bool
|
||||||
var playSounds: Bool
|
var playSounds: Bool
|
||||||
|
|
||||||
// App Store detection (cached at launch, not persisted)
|
|
||||||
var isAppStoreVersion: Bool
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
lookAwayTimer: TimerConfiguration = TimerConfiguration(
|
lookAwayTimer: TimerConfiguration = TimerConfiguration(
|
||||||
enabled: true, intervalSeconds: 20 * 60),
|
enabled: true, intervalSeconds: 20 * 60),
|
||||||
@@ -61,8 +58,7 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
subtleReminderSize: ReminderSize = .medium,
|
subtleReminderSize: ReminderSize = .medium,
|
||||||
hasCompletedOnboarding: Bool = false,
|
hasCompletedOnboarding: Bool = false,
|
||||||
launchAtLogin: Bool = false,
|
launchAtLogin: Bool = false,
|
||||||
playSounds: Bool = true,
|
playSounds: Bool = true
|
||||||
isAppStoreVersion: Bool = true
|
|
||||||
) {
|
) {
|
||||||
self.lookAwayTimer = lookAwayTimer
|
self.lookAwayTimer = lookAwayTimer
|
||||||
self.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
self.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
||||||
@@ -73,7 +69,6 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
self.hasCompletedOnboarding = hasCompletedOnboarding
|
self.hasCompletedOnboarding = hasCompletedOnboarding
|
||||||
self.launchAtLogin = launchAtLogin
|
self.launchAtLogin = launchAtLogin
|
||||||
self.playSounds = playSounds
|
self.playSounds = playSounds
|
||||||
self.isAppStoreVersion = isAppStoreVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var defaults: AppSettings {
|
static var defaults: AppSettings {
|
||||||
@@ -86,67 +81,7 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
subtleReminderSize: .medium,
|
subtleReminderSize: .medium,
|
||||||
hasCompletedOnboarding: false,
|
hasCompletedOnboarding: false,
|
||||||
launchAtLogin: false,
|
launchAtLogin: false,
|
||||||
playSounds: true,
|
playSounds: true
|
||||||
isAppStoreVersion: false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manual Equatable implementation required because isAppStoreVersion
|
|
||||||
/// is excluded from Codable persistence but included in equality checks
|
|
||||||
static func == (lhs: AppSettings, rhs: AppSettings) -> Bool {
|
|
||||||
lhs.lookAwayTimer == rhs.lookAwayTimer
|
|
||||||
&& lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds
|
|
||||||
&& lhs.blinkTimer == rhs.blinkTimer
|
|
||||||
&& lhs.postureTimer == rhs.postureTimer
|
|
||||||
&& lhs.userTimers == rhs.userTimers
|
|
||||||
&& lhs.subtleReminderSize == rhs.subtleReminderSize
|
|
||||||
&& lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding
|
|
||||||
&& lhs.launchAtLogin == rhs.launchAtLogin
|
|
||||||
&& lhs.playSounds == rhs.playSounds
|
|
||||||
&& lhs.isAppStoreVersion == rhs.isAppStoreVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Custom Codable Implementation
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case lookAwayTimer
|
|
||||||
case lookAwayCountdownSeconds
|
|
||||||
case blinkTimer
|
|
||||||
case postureTimer
|
|
||||||
case userTimers
|
|
||||||
case subtleReminderSize
|
|
||||||
case hasCompletedOnboarding
|
|
||||||
case launchAtLogin
|
|
||||||
case playSounds
|
|
||||||
// isAppStoreVersion is intentionally excluded from persistence
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
lookAwayTimer = try container.decode(TimerConfiguration.self, forKey: .lookAwayTimer)
|
|
||||||
lookAwayCountdownSeconds = try container.decode(Int.self, forKey: .lookAwayCountdownSeconds)
|
|
||||||
blinkTimer = try container.decode(TimerConfiguration.self, forKey: .blinkTimer)
|
|
||||||
postureTimer = try container.decode(TimerConfiguration.self, forKey: .postureTimer)
|
|
||||||
userTimers = try container.decode([UserTimer].self, forKey: .userTimers)
|
|
||||||
subtleReminderSize = try container.decode(ReminderSize.self, forKey: .subtleReminderSize)
|
|
||||||
hasCompletedOnboarding = try container.decode(Bool.self, forKey: .hasCompletedOnboarding)
|
|
||||||
launchAtLogin = try container.decode(Bool.self, forKey: .launchAtLogin)
|
|
||||||
playSounds = try container.decode(Bool.self, forKey: .playSounds)
|
|
||||||
// isAppStoreVersion is not persisted, will be set at launch
|
|
||||||
isAppStoreVersion = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(lookAwayTimer, forKey: .lookAwayTimer)
|
|
||||||
try container.encode(lookAwayCountdownSeconds, forKey: .lookAwayCountdownSeconds)
|
|
||||||
try container.encode(blinkTimer, forKey: .blinkTimer)
|
|
||||||
try container.encode(postureTimer, forKey: .postureTimer)
|
|
||||||
try container.encode(userTimers, forKey: .userTimers)
|
|
||||||
try container.encode(subtleReminderSize, forKey: .subtleReminderSize)
|
|
||||||
try container.encode(hasCompletedOnboarding, forKey: .hasCompletedOnboarding)
|
|
||||||
try container.encode(launchAtLogin, forKey: .launchAtLogin)
|
|
||||||
try container.encode(playSounds, forKey: .playSounds)
|
|
||||||
// isAppStoreVersion is intentionally not persisted
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
//
|
|
||||||
// AppStoreDetector.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Created by Mike Freno on 1/10/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import StoreKit
|
|
||||||
|
|
||||||
enum AppStoreDetector {
|
|
||||||
/// Returns true if the app was downloaded from the Mac App Store.
|
|
||||||
///
|
|
||||||
/// Uses StoreKit's AppTransaction API on macOS 15+ to verify if the app is an App Store version.
|
|
||||||
/// Falls back to a heuristic receipt check on macOS versions prior to 15.
|
|
||||||
///
|
|
||||||
/// This method is asynchronous due to the use of StoreKit's async API.
|
|
||||||
static func isAppStoreVersion() async -> Bool {
|
|
||||||
#if DEBUG
|
|
||||||
return false
|
|
||||||
#else
|
|
||||||
if #available(macOS 15.0, *) {
|
|
||||||
do {
|
|
||||||
let transaction = try await AppTransaction.shared
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback for older macOS: use legacy receipt check
|
|
||||||
|
|
||||||
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let fileExists = FileManager.default.fileExists(atPath: receiptURL.path)
|
|
||||||
guard fileExists else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let receiptData = try? Data(contentsOf: receiptURL),
|
|
||||||
receiptData.count > 2
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = [UInt8](receiptData.prefix(2))
|
|
||||||
let isValid = bytes[0] == 0x30 && bytes[1] == 0x82
|
|
||||||
return isValid
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the app is running in TestFlight.
|
|
||||||
///
|
|
||||||
/// On macOS 15+, StoreKit does not expose a TestFlight receipt type.
|
|
||||||
/// This method returns false on macOS 15+ as a result.
|
|
||||||
/// On earlier versions, it checks for the presence of a "sandboxReceipt".
|
|
||||||
///
|
|
||||||
/// This method is asynchronous for API consistency.
|
|
||||||
static func isTestFlight() async -> Bool {
|
|
||||||
#if DEBUG
|
|
||||||
return false
|
|
||||||
#else
|
|
||||||
if #available(macOS 15.0, *) {
|
|
||||||
// StoreKit does not expose TestFlight receipt type.
|
|
||||||
// As a workaround, fallback to legacy method if available, else return false.
|
|
||||||
return false // No supported TestFlight check post-macOS 15
|
|
||||||
} else {
|
|
||||||
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -74,9 +74,6 @@ class SettingsManager: ObservableObject {
|
|||||||
/// Use this for critical save points like app termination or system sleep.
|
/// Use this for critical save points like app termination or system sleep.
|
||||||
func saveImmediately() {
|
func saveImmediately() {
|
||||||
save()
|
save()
|
||||||
// Cancel any pending debounced saves
|
|
||||||
saveCancellable?.cancel()
|
|
||||||
setupDebouncedSave()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
@@ -120,13 +117,4 @@ class SettingsManager: ObservableObject {
|
|||||||
preconditionFailure("Missing timer configuration mappings for: \(missing)")
|
preconditionFailure("Missing timer configuration mappings for: \(missing)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detects and caches the App Store version status.
|
|
||||||
/// This should be called once at app launch to avoid async checks throughout the app.
|
|
||||||
func detectAppStoreVersion() async {
|
|
||||||
let isAppStore = await AppStoreDetector.isAppStoreVersion()
|
|
||||||
await MainActor.run {
|
|
||||||
settings.isAppStoreVersion = isAppStore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
#if !APPSTORE
|
#if !APPSTORE
|
||||||
import Sparkle
|
import Sparkle
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -123,10 +123,9 @@ struct OnboardingContainerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 1000,
|
minWidth: 1000,
|
||||||
minHeight: settingsManager.settings.isAppStoreVersion ? 700 : 900
|
minHeight: 700
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ struct SettingsWindowView: View {
|
|||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
minWidth: 750,
|
minWidth: 750,
|
||||||
minHeight: settingsManager.settings.isAppStoreVersion ? 700 : 900
|
minHeight: 700
|
||||||
)
|
)
|
||||||
.onReceive(
|
.onReceive(
|
||||||
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ struct GeneralSetupView: View {
|
|||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
// Software Updates Section
|
// Software Updates Section
|
||||||
if !settingsManager.settings.isAppStoreVersion {
|
#if !APPSTORE
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Software Updates")
|
Text("Software Updates")
|
||||||
@@ -90,7 +90,7 @@ struct GeneralSetupView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
}
|
#endif
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Subtle Reminder Size")
|
Text("Subtle Reminder Size")
|
||||||
@@ -174,7 +174,7 @@ struct GeneralSetupView: View {
|
|||||||
.glassEffectIfAvailable(
|
.glassEffectIfAvailable(
|
||||||
GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||||
|
|
||||||
if !settingsManager.settings.isAppStoreVersion {
|
#if !APPSTORE
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if let url = URL(string: "https://buymeacoffee.com/mikefreno") {
|
if let url = URL(string: "https://buymeacoffee.com/mikefreno") {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
@@ -205,7 +205,7 @@ struct GeneralSetupView: View {
|
|||||||
.glassEffectIfAvailable(
|
.glassEffectIfAvailable(
|
||||||
GlassStyle.regular.tint(.orange).interactive(),
|
GlassStyle.regular.tint(.orange).interactive(),
|
||||||
in: .rect(cornerRadius: 10))
|
in: .rect(cornerRadius: 10))
|
||||||
}
|
#endif
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// AppStoreDetectorTests.swift
|
|
||||||
// GazeTests
|
|
||||||
//
|
|
||||||
// Created by Mike Freno on 1/10/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
@testable import Gaze
|
|
||||||
import Testing
|
|
||||||
|
|
||||||
struct AppStoreDetectorTests {
|
|
||||||
|
|
||||||
@Test func isAppStoreVersionReturnsFalseInDebug() async {
|
|
||||||
// In test/debug builds, should always return false
|
|
||||||
#expect(await AppStoreDetector.isAppStoreVersion() == false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func isTestFlightReturnsFalseInDebug() async {
|
|
||||||
// In test/debug builds, should always return false
|
|
||||||
#expect(await AppStoreDetector.isTestFlight() == false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func receiptValidationHandlesMissingReceipt() async {
|
|
||||||
// When there's no receipt (development build), should return false
|
|
||||||
// This is implicitly tested by isAppStoreVersionReturnsFalseInDebug
|
|
||||||
// but we're documenting the expected behavior
|
|
||||||
#expect(await AppStoreDetector.isAppStoreVersion() == false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -306,4 +306,106 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
|
XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
|
||||||
XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)])
|
XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testMultipleReminderTypesCanTriggerSimultaneously() {
|
||||||
|
// Setup: Create a user timer with overlay type (focus-stealing)
|
||||||
|
let overlayTimer = UserTimer(
|
||||||
|
title: "Water Break",
|
||||||
|
type: .overlay,
|
||||||
|
timeOnScreenSeconds: 10,
|
||||||
|
intervalMinutes: 1,
|
||||||
|
message: "Drink water"
|
||||||
|
)
|
||||||
|
settingsManager.settings.userTimers = [overlayTimer]
|
||||||
|
|
||||||
|
timerEngine.start()
|
||||||
|
|
||||||
|
// Trigger an overlay reminder (look away or user timer overlay)
|
||||||
|
timerEngine.triggerReminder(for: .user(id: overlayTimer.id))
|
||||||
|
|
||||||
|
// Verify overlay reminder is active
|
||||||
|
XCTAssertNotNil(timerEngine.activeReminder)
|
||||||
|
if case .userTimerTriggered(let timer) = timerEngine.activeReminder {
|
||||||
|
XCTAssertEqual(timer.id, overlayTimer.id)
|
||||||
|
XCTAssertEqual(timer.type, .overlay)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected userTimerTriggered with overlay type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the overlay timer is paused
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id)))
|
||||||
|
|
||||||
|
// Now trigger a subtle reminder (blink) while overlay is still active
|
||||||
|
let previousActiveReminder = timerEngine.activeReminder
|
||||||
|
timerEngine.triggerReminder(for: .builtIn(.blink))
|
||||||
|
|
||||||
|
// The activeReminder should be replaced with the blink reminder
|
||||||
|
// This is expected behavior - TimerEngine only tracks one activeReminder
|
||||||
|
XCTAssertNotNil(timerEngine.activeReminder)
|
||||||
|
if case .blinkTriggered = timerEngine.activeReminder {
|
||||||
|
XCTAssertTrue(true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected blinkTriggered reminder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both timers should be paused (the one that triggered their reminder)
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id)))
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.blink)))
|
||||||
|
|
||||||
|
// The key insight: Even though TimerEngine only tracks one activeReminder,
|
||||||
|
// AppDelegate now tracks overlay and subtle windows separately, so both
|
||||||
|
// reminders can be displayed simultaneously without interference
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOverlayReminderDoesNotBlockSubtleReminders() {
|
||||||
|
// This test verifies the fix for the bug where a subtle reminder
|
||||||
|
// would cause an overlay reminder to get stuck
|
||||||
|
|
||||||
|
// Setup overlay user timer
|
||||||
|
let overlayTimer = UserTimer(
|
||||||
|
title: "Stand Up",
|
||||||
|
type: .overlay,
|
||||||
|
timeOnScreenSeconds: 10,
|
||||||
|
intervalMinutes: 1
|
||||||
|
)
|
||||||
|
settingsManager.settings.userTimers = [overlayTimer]
|
||||||
|
settingsManager.settings.blinkTimer.enabled = true
|
||||||
|
settingsManager.settings.blinkTimer.intervalSeconds = 60
|
||||||
|
|
||||||
|
timerEngine.start()
|
||||||
|
|
||||||
|
// Trigger overlay reminder first
|
||||||
|
timerEngine.triggerReminder(for: .user(id: overlayTimer.id))
|
||||||
|
XCTAssertNotNil(timerEngine.activeReminder)
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id)))
|
||||||
|
|
||||||
|
// Trigger subtle reminder while overlay is active
|
||||||
|
timerEngine.triggerReminder(for: .builtIn(.blink))
|
||||||
|
|
||||||
|
// The blink reminder should now be active
|
||||||
|
if case .blinkTriggered = timerEngine.activeReminder {
|
||||||
|
XCTAssertTrue(true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected blinkTriggered reminder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both timers should be paused
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id)))
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.blink)))
|
||||||
|
|
||||||
|
// Dismiss the blink reminder
|
||||||
|
timerEngine.dismissReminder()
|
||||||
|
|
||||||
|
// After dismissing blink, the reminder should be cleared
|
||||||
|
XCTAssertNil(timerEngine.activeReminder)
|
||||||
|
|
||||||
|
// Blink timer should be reset and resumed
|
||||||
|
XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.blink)))
|
||||||
|
XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 60)
|
||||||
|
|
||||||
|
// The overlay timer should still be paused (user needs to dismiss it manually)
|
||||||
|
// Note: In the actual app, AppDelegate tracks this window separately and it
|
||||||
|
// remains visible even after the subtle reminder dismisses
|
||||||
|
XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user