fix: build conformed with corrected app store detector
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
||||||
2759160C2F132C7A00D0E60D /* Gaze.icon in Resources */ = {isa = PBXBuildFile; fileRef = 2759160B2F132C7A00D0E60D /* Gaze.icon */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -29,7 +28,6 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
2759160B2F132C7A00D0E60D /* Gaze.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Gaze.icon; sourceTree = "<group>"; };
|
|
||||||
27A21B3C2F0F69DC0018C4F3 /* Gaze.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gaze.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
27A21B3C2F0F69DC0018C4F3 /* Gaze.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gaze.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
27A21B492F0F69DD0018C4F3 /* GazeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
27A21B492F0F69DD0018C4F3 /* GazeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
27A21B532F0F69DD0018C4F3 /* GazeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
27A21B532F0F69DD0018C4F3 /* GazeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -82,7 +80,6 @@
|
|||||||
27A21B332F0F69DC0018C4F3 = {
|
27A21B332F0F69DC0018C4F3 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2759160B2F132C7A00D0E60D /* Gaze.icon */,
|
|
||||||
27A21B3E2F0F69DC0018C4F3 /* Gaze */,
|
27A21B3E2F0F69DC0018C4F3 /* Gaze */,
|
||||||
27A21B4C2F0F69DD0018C4F3 /* GazeTests */,
|
27A21B4C2F0F69DD0018C4F3 /* GazeTests */,
|
||||||
27A21B562F0F69DD0018C4F3 /* GazeUITests */,
|
27A21B562F0F69DD0018C4F3 /* GazeUITests */,
|
||||||
@@ -224,7 +221,6 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
2759160C2F132C7A00D0E60D /* Gaze.icon in Resources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -405,7 +401,9 @@
|
|||||||
27A21B5E2F0F69DD0018C4F3 /* Debug */ = {
|
27A21B5E2F0F69DD0018C4F3 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -415,6 +413,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -436,7 +435,9 @@
|
|||||||
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
|
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -446,6 +447,7 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_USER_SELECTED_FILES = readonly;
|
ENABLE_USER_SELECTED_FILES = readonly;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
settingsManager = SettingsManager.shared
|
settingsManager = SettingsManager.shared
|
||||||
timerEngine = TimerEngine(settingsManager: settingsManager!)
|
timerEngine = TimerEngine(settingsManager: settingsManager!)
|
||||||
|
|
||||||
|
// Detect App Store version asynchronously at launch
|
||||||
|
Task {
|
||||||
|
await settingsManager?.detectAppStoreVersion()
|
||||||
|
}
|
||||||
|
|
||||||
setupLifecycleObservers()
|
setupLifecycleObservers()
|
||||||
observeSettingsChanges()
|
observeSettingsChanges()
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 178 B After Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 934 B After Width: | Height: | Size: 934 B |
|
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 899 B |
|
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 884 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 937 B |
|
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 621 B |
|
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
@@ -166,6 +166,23 @@
|
|||||||
"hidden" : false,
|
"hidden" : false,
|
||||||
"layers" : [
|
"layers" : [
|
||||||
{
|
{
|
||||||
|
"blend-mode" : "darken",
|
||||||
|
"fill" : {
|
||||||
|
"linear-gradient" : [
|
||||||
|
"extended-srgb:0.00000,0.53333,1.00000,1.00000",
|
||||||
|
"display-p3:0.38403,0.64839,1.04685,1.00000"
|
||||||
|
],
|
||||||
|
"orientation" : {
|
||||||
|
"start" : {
|
||||||
|
"x" : 0.5,
|
||||||
|
"y" : 0
|
||||||
|
},
|
||||||
|
"stop" : {
|
||||||
|
"x" : 0.5,
|
||||||
|
"y" : 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"glass" : false,
|
"glass" : false,
|
||||||
"image-name" : "Line 7.svg",
|
"image-name" : "Line 7.svg",
|
||||||
"name" : "Line 7",
|
"name" : "Line 7",
|
||||||
@@ -186,7 +203,7 @@
|
|||||||
"image-name" : "Line 8.svg",
|
"image-name" : "Line 8.svg",
|
||||||
"name" : "Line 8",
|
"name" : "Line 8",
|
||||||
"position" : {
|
"position" : {
|
||||||
"scale" : 1.21,
|
"scale" : 1.26,
|
||||||
"translation-in-points" : [
|
"translation-in-points" : [
|
||||||
-0.1999973177900074,
|
-0.1999973177900074,
|
||||||
-82.12440000000004
|
-82.12440000000004
|
||||||
@@ -231,7 +248,7 @@
|
|||||||
"image-name" : "Ellipse 11.svg",
|
"image-name" : "Ellipse 11.svg",
|
||||||
"name" : "Ellipse 11",
|
"name" : "Ellipse 11",
|
||||||
"position" : {
|
"position" : {
|
||||||
"scale" : 0.99,
|
"scale" : 0.83,
|
||||||
"translation-in-points" : [
|
"translation-in-points" : [
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
@@ -50,6 +50,9 @@ 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),
|
||||||
@@ -62,7 +65,8 @@ 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 = false
|
||||||
) {
|
) {
|
||||||
self.lookAwayTimer = lookAwayTimer
|
self.lookAwayTimer = lookAwayTimer
|
||||||
self.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
self.lookAwayCountdownSeconds = lookAwayCountdownSeconds
|
||||||
@@ -73,6 +77,7 @@ 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 {
|
||||||
@@ -85,7 +90,8 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
subtleReminderSize: .medium,
|
subtleReminderSize: .medium,
|
||||||
hasCompletedOnboarding: false,
|
hasCompletedOnboarding: false,
|
||||||
launchAtLogin: false,
|
launchAtLogin: false,
|
||||||
playSounds: true
|
playSounds: true,
|
||||||
|
isAppStoreVersion: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,5 +103,50 @@ struct AppSettings: Codable, Equatable, Hashable {
|
|||||||
&& lhs.subtleReminderSize == rhs.subtleReminderSize
|
&& lhs.subtleReminderSize == rhs.subtleReminderSize
|
||||||
&& lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding
|
&& lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding
|
||||||
&& lhs.launchAtLogin == rhs.launchAtLogin && lhs.playSounds == rhs.playSounds
|
&& 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,47 +6,63 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
enum AppStoreDetector {
|
enum AppStoreDetector {
|
||||||
/// Returns true if the app was downloaded from the Mac App Store
|
/// Returns true if the app was downloaded from the Mac App Store.
|
||||||
///
|
///
|
||||||
/// Uses a heuristic approach that checks for the presence of a valid App Store receipt.
|
/// Uses StoreKit's AppTransaction API on macOS 15+ to verify if the app is an App Store version.
|
||||||
/// This is sufficient for distinguishing App Store builds from direct distribution.
|
/// Falls back to a heuristic receipt check on macOS versions prior to 15.
|
||||||
///
|
///
|
||||||
/// Note: This does not perform full cryptographic validation of the receipt signature, its
|
/// This method is asynchronous due to the use of StoreKit's async API.
|
||||||
/// only used to determine if we should show the 'buy me a coffee' link.
|
static func isAppStoreVersion() async -> Bool {
|
||||||
static var isAppStoreVersion: Bool {
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
return false
|
return false
|
||||||
#else
|
#else
|
||||||
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
|
if #available(macOS 15.0, *) {
|
||||||
return false
|
do {
|
||||||
|
_ = 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
|
||||||
|
}
|
||||||
|
guard FileManager.default.fileExists(atPath: receiptURL.path) else {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: receiptURL.path) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let receiptData = try? Data(contentsOf: receiptURL),
|
|
||||||
receiptData.count > 2
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify receipt has PKCS#7 signature format (starts with ASN.1 SEQUENCE tag)
|
|
||||||
let bytes = [UInt8](receiptData.prefix(2))
|
|
||||||
return bytes[0] == 0x30 && bytes[1] == 0x82
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the app is running in TestFlight
|
/// Checks if the app is running in TestFlight.
|
||||||
///
|
///
|
||||||
/// TestFlight builds have a receipt named "sandboxReceipt" instead of "receipt"
|
/// On macOS 15+, StoreKit does not expose a TestFlight receipt type.
|
||||||
static var isTestFlight: Bool {
|
/// 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
|
#if DEBUG
|
||||||
return false
|
return false
|
||||||
#else
|
#else
|
||||||
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
|
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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class Version101Migration: Migration {
|
|||||||
var targetVersion: String = "1.0.1"
|
var targetVersion: String = "1.0.1"
|
||||||
|
|
||||||
func migrate(_ data: [String: Any]) throws -> [String: Any] {
|
func migrate(_ data: [String: Any]) throws -> [String: Any] {
|
||||||
var migratedData = data
|
let migratedData = data
|
||||||
|
|
||||||
// Example migration logic:
|
// Example migration logic:
|
||||||
// Add any new fields with default values if they don't exist
|
// Add any new fields with default values if they don't exist
|
||||||
|
|||||||
@@ -75,4 +75,13 @@ class SettingsManager: ObservableObject {
|
|||||||
settings.postureTimer = configuration
|
settings.postureTimer = configuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ struct OnboardingContainerView: View {
|
|||||||
SettingsOnboardingView(
|
SettingsOnboardingView(
|
||||||
launchAtLogin: $launchAtLogin,
|
launchAtLogin: $launchAtLogin,
|
||||||
subtleReminderSize: $subtleReminderSize,
|
subtleReminderSize: $subtleReminderSize,
|
||||||
|
isAppStoreVersion: Binding(
|
||||||
|
get: { settingsManager.settings.isAppStoreVersion },
|
||||||
|
set: { _ in }
|
||||||
|
),
|
||||||
isOnboarding: true
|
isOnboarding: true
|
||||||
)
|
)
|
||||||
.tag(4)
|
.tag(4)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct SettingsOnboardingView: View {
|
struct SettingsOnboardingView: View {
|
||||||
@Binding var launchAtLogin: Bool
|
@Binding var launchAtLogin: Bool
|
||||||
@Binding var subtleReminderSize: ReminderSize
|
@Binding var subtleReminderSize: ReminderSize
|
||||||
|
@Binding var isAppStoreVersion: Bool
|
||||||
var isOnboarding: Bool = true
|
var isOnboarding: Bool = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -131,7 +132,7 @@ struct SettingsOnboardingView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||||
|
|
||||||
if !AppStoreDetector.isAppStoreVersion {
|
if !isAppStoreVersion {
|
||||||
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)
|
||||||
@@ -198,6 +199,7 @@ struct SettingsOnboardingView: View {
|
|||||||
SettingsOnboardingView(
|
SettingsOnboardingView(
|
||||||
launchAtLogin: .constant(false),
|
launchAtLogin: .constant(false),
|
||||||
subtleReminderSize: .constant(.medium),
|
subtleReminderSize: .constant(.medium),
|
||||||
|
isAppStoreVersion: .constant(false),
|
||||||
isOnboarding: true
|
isOnboarding: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -206,6 +208,7 @@ struct SettingsOnboardingView: View {
|
|||||||
SettingsOnboardingView(
|
SettingsOnboardingView(
|
||||||
launchAtLogin: .constant(true),
|
launchAtLogin: .constant(true),
|
||||||
subtleReminderSize: .constant(.medium),
|
subtleReminderSize: .constant(.medium),
|
||||||
|
isAppStoreVersion: .constant(false),
|
||||||
isOnboarding: true
|
isOnboarding: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ struct SettingsWindowView: View {
|
|||||||
SettingsOnboardingView(
|
SettingsOnboardingView(
|
||||||
launchAtLogin: $launchAtLogin,
|
launchAtLogin: $launchAtLogin,
|
||||||
subtleReminderSize: $subtleReminderSize,
|
subtleReminderSize: $subtleReminderSize,
|
||||||
|
isAppStoreVersion: Binding(
|
||||||
|
get: { settingsManager.settings.isAppStoreVersion },
|
||||||
|
set: { _ in }
|
||||||
|
),
|
||||||
isOnboarding: false
|
isOnboarding: false
|
||||||
)
|
)
|
||||||
.tag(4)
|
.tag(4)
|
||||||
@@ -140,7 +144,8 @@ struct SettingsWindowView: View {
|
|||||||
subtleReminderSize: subtleReminderSize,
|
subtleReminderSize: subtleReminderSize,
|
||||||
hasCompletedOnboarding: settingsManager.settings.hasCompletedOnboarding,
|
hasCompletedOnboarding: settingsManager.settings.hasCompletedOnboarding,
|
||||||
launchAtLogin: launchAtLogin,
|
launchAtLogin: launchAtLogin,
|
||||||
playSounds: settingsManager.settings.playSounds
|
playSounds: settingsManager.settings.playSounds,
|
||||||
|
isAppStoreVersion: settingsManager.settings.isAppStoreVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assign the entire settings object to trigger didSet and observers
|
// Assign the entire settings object to trigger didSet and observers
|
||||||
|
|||||||
@@ -10,20 +10,20 @@ import Testing
|
|||||||
|
|
||||||
struct AppStoreDetectorTests {
|
struct AppStoreDetectorTests {
|
||||||
|
|
||||||
@Test func isAppStoreVersionReturnsFalseInDebug() {
|
@Test func isAppStoreVersionReturnsFalseInDebug() async {
|
||||||
// In test/debug builds, should always return false
|
// In test/debug builds, should always return false
|
||||||
#expect(AppStoreDetector.isAppStoreVersion == false)
|
#expect(await AppStoreDetector.isAppStoreVersion() == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func isTestFlightReturnsFalseInDebug() {
|
@Test func isTestFlightReturnsFalseInDebug() async {
|
||||||
// In test/debug builds, should always return false
|
// In test/debug builds, should always return false
|
||||||
#expect(AppStoreDetector.isTestFlight == false)
|
#expect(await AppStoreDetector.isTestFlight() == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func receiptValidationHandlesMissingReceipt() {
|
@Test func receiptValidationHandlesMissingReceipt() async {
|
||||||
// When there's no receipt (development build), should return false
|
// When there's no receipt (development build), should return false
|
||||||
// This is implicitly tested by isAppStoreVersionReturnsFalseInDebug
|
// This is implicitly tested by isAppStoreVersionReturnsFalseInDebug
|
||||||
// but we're documenting the expected behavior
|
// but we're documenting the expected behavior
|
||||||
#expect(AppStoreDetector.isAppStoreVersion == false)
|
#expect(await AppStoreDetector.isAppStoreVersion() == false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||