Merge fix_sub into main
Brings subscription state fixes and version 1.0.2 bump. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -928,7 +928,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Reflect Watch App/Reflect Watch AppDebug.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Reflect Watch App/Reflect Watch AppDebug.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -940,7 +940,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug.watch;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug.watch;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -1084,7 +1084,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Reflect (iOS)Dev.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Reflect (iOS)Dev.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1106,7 +1106,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug;
|
||||||
PRODUCT_NAME = Reflect;
|
PRODUCT_NAME = Reflect;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1126,7 +1126,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Reflect (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Reflect (iOS).entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1148,7 +1148,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect;
|
||||||
PRODUCT_NAME = Reflect;
|
PRODUCT_NAME = Reflect;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1169,7 +1169,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -1182,7 +1182,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect;
|
||||||
PRODUCT_NAME = Reflect;
|
PRODUCT_NAME = Reflect;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -1200,7 +1200,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -1213,7 +1213,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug;
|
||||||
PRODUCT_NAME = Reflect;
|
PRODUCT_NAME = Reflect;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -1226,10 +1226,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1244,10 +1244,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1263,11 +1263,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-macOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-macOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -1281,11 +1281,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-macOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.88oak.Tests-macOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -1303,7 +1303,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ReflectWidgetExtensionDev.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ReflectWidgetExtensionDev.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "ReflectWidgetExtension-Info.plist";
|
INFOPLIST_FILE = "ReflectWidgetExtension-Info.plist";
|
||||||
@@ -1316,7 +1316,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug.widget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.debug.widget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1337,7 +1337,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ReflectWidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ReflectWidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "ReflectWidgetExtension-Info.plist";
|
INFOPLIST_FILE = "ReflectWidgetExtension-Info.plist";
|
||||||
@@ -1350,7 +1350,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.widget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.widget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1389,7 +1389,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Reflect Watch App/Reflect Watch App.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Reflect Watch App/Reflect Watch App.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1.1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = QND55P4443;
|
DEVELOPMENT_TEAM = QND55P4443;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1401,7 +1401,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.watch;
|
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.reflect.watch;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
|
|||||||
@@ -159,11 +159,16 @@ class IAPManager: ObservableObject {
|
|||||||
|
|
||||||
/// Check subscription status - call on app launch and when becoming active
|
/// Check subscription status - call on app launch and when becoming active
|
||||||
/// Throttled to avoid excessive StoreKit calls on rapid foreground transitions
|
/// Throttled to avoid excessive StoreKit calls on rapid foreground transitions
|
||||||
func checkSubscriptionStatus() async {
|
/// Pass `force: true` to bypass throttle (e.g. after a transaction update)
|
||||||
// Throttle: skip if we checked recently (unless state is unknown)
|
func checkSubscriptionStatus(force: Bool = false) async {
|
||||||
if state != .unknown,
|
AppLogger.iap.debug("checkSubscriptionStatus: called (force=\(force), currentState=\(String(describing: self.state)))")
|
||||||
|
|
||||||
|
// Throttle: skip if we checked recently (unless state is unknown or forced)
|
||||||
|
if !force,
|
||||||
|
state != .unknown,
|
||||||
let lastCheck = lastStatusCheckTime,
|
let lastCheck = lastStatusCheckTime,
|
||||||
Date().timeIntervalSince(lastCheck) < statusCheckInterval {
|
Date().timeIntervalSince(lastCheck) < statusCheckInterval {
|
||||||
|
AppLogger.iap.debug("checkSubscriptionStatus: THROTTLED (last check \(Date().timeIntervalSince(lastCheck))s ago)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +177,7 @@ class IAPManager: ObservableObject {
|
|||||||
defer {
|
defer {
|
||||||
if isLoading { isLoading = false }
|
if isLoading { isLoading = false }
|
||||||
lastStatusCheckTime = Date()
|
lastStatusCheckTime = Date()
|
||||||
|
AppLogger.iap.debug("checkSubscriptionStatus: DONE — final state=\(String(describing: self.state))")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch available products
|
// Fetch available products
|
||||||
@@ -179,6 +185,7 @@ class IAPManager: ObservableObject {
|
|||||||
|
|
||||||
// Check for active subscription
|
// Check for active subscription
|
||||||
let hasActiveSubscription = await checkForActiveSubscription()
|
let hasActiveSubscription = await checkForActiveSubscription()
|
||||||
|
AppLogger.iap.debug("checkSubscriptionStatus: hasActiveSubscription=\(hasActiveSubscription), state after check=\(String(describing: self.state))")
|
||||||
|
|
||||||
if hasActiveSubscription {
|
if hasActiveSubscription {
|
||||||
// State already set in checkForActiveSubscription — cache it
|
// State already set in checkForActiveSubscription — cache it
|
||||||
@@ -195,29 +202,24 @@ class IAPManager: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve terminal StoreKit states (expired/revoked) instead of overriding with trial fallback.
|
|
||||||
if case .expired = state {
|
|
||||||
syncSubscriptionStatusToUserDefaults()
|
|
||||||
trackSubscriptionAnalytics(source: "status_check_terminal")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if case .revoked = state {
|
|
||||||
syncSubscriptionStatusToUserDefaults()
|
|
||||||
trackSubscriptionAnalytics(source: "status_check_terminal")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live check found no active subscription.
|
// Live check found no active subscription.
|
||||||
// Before downgrading, check if cached expiration is still valid (covers offline/transient failures).
|
// Only trust cached expiration if we're offline (products failed to load).
|
||||||
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
// If products loaded successfully, StoreKit had server access and the live result is authoritative.
|
||||||
if let expiration = cachedExpiration, expiration > Date() {
|
if availableProducts.isEmpty {
|
||||||
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
||||||
syncSubscriptionStatusToUserDefaults()
|
AppLogger.iap.debug("checkSubscriptionStatus: offline (no products) — cachedExpiration=\(cachedExpiration?.description ?? "nil")")
|
||||||
trackSubscriptionAnalytics(source: "status_check_cached")
|
|
||||||
return
|
if let expiration = cachedExpiration, expiration > Date() {
|
||||||
|
AppLogger.iap.debug("checkSubscriptionStatus: using cached expiration (offline, still valid)")
|
||||||
|
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
||||||
|
syncSubscriptionStatusToUserDefaults()
|
||||||
|
trackSubscriptionAnalytics(source: "status_check_cached")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscription genuinely gone — clear cache and fall back to trial
|
// Subscription genuinely gone — clear cache and fall back to trial
|
||||||
|
AppLogger.iap.debug("checkSubscriptionStatus: falling back to trial — firstLaunchDate=\(self.firstLaunchDate), trialDays=\(self.trialDays)")
|
||||||
cacheSubscriptionExpiration(nil)
|
cacheSubscriptionExpiration(nil)
|
||||||
updateTrialState()
|
updateTrialState()
|
||||||
trackSubscriptionAnalytics(source: "status_check_fallback")
|
trackSubscriptionAnalytics(source: "status_check_fallback")
|
||||||
@@ -237,16 +239,24 @@ class IAPManager: ObservableObject {
|
|||||||
/// Restore cached subscription state on launch (before async check completes)
|
/// Restore cached subscription state on launch (before async check completes)
|
||||||
private func restoreCachedSubscriptionState() {
|
private func restoreCachedSubscriptionState() {
|
||||||
let hasActive = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
let hasActive = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
||||||
guard hasActive else { return }
|
|
||||||
|
|
||||||
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
|
||||||
|
AppLogger.iap.debug("restoreCachedSubscriptionState: hasActive=\(hasActive), cachedExpiration=\(cachedExpiration?.description ?? "nil")")
|
||||||
|
|
||||||
|
guard hasActive else {
|
||||||
|
AppLogger.iap.debug("restoreCachedSubscriptionState: no cached active state, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a cached expiration and it's still in the future, restore subscribed state
|
// If we have a cached expiration and it's still in the future, restore subscribed state
|
||||||
if let expiration = cachedExpiration, expiration > Date() {
|
if let expiration = cachedExpiration, expiration > Date() {
|
||||||
|
AppLogger.iap.debug("restoreCachedSubscriptionState: restoring .subscribed (cached expiration in future)")
|
||||||
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
state = .subscribed(expirationDate: expiration, willAutoRenew: false)
|
||||||
} else if cachedExpiration == nil && hasActive {
|
} else if cachedExpiration == nil && hasActive {
|
||||||
// Had access but no expiration cached (e.g., upgraded from older version) — trust it
|
// Had access but no expiration cached (e.g., upgraded from older version) — trust it
|
||||||
|
AppLogger.iap.debug("restoreCachedSubscriptionState: restoring .subscribed (no expiration, trusting hasActive)")
|
||||||
state = .subscribed(expirationDate: nil, willAutoRenew: false)
|
state = .subscribed(expirationDate: nil, willAutoRenew: false)
|
||||||
|
} else {
|
||||||
|
AppLogger.iap.debug("restoreCachedSubscriptionState: cached expiration in past, not restoring")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +264,7 @@ class IAPManager: ObservableObject {
|
|||||||
func restore(source: String = "settings") async {
|
func restore(source: String = "settings") async {
|
||||||
do {
|
do {
|
||||||
try await AppStore.sync()
|
try await AppStore.sync()
|
||||||
await checkSubscriptionStatus()
|
await checkSubscriptionStatus(force: true)
|
||||||
AnalyticsManager.shared.trackPurchaseRestored(source: source)
|
AnalyticsManager.shared.trackPurchaseRestored(source: source)
|
||||||
trackSubscriptionAnalytics(source: "restore")
|
trackSubscriptionAnalytics(source: "restore")
|
||||||
} catch {
|
} catch {
|
||||||
@@ -377,10 +387,13 @@ class IAPManager: ObservableObject {
|
|||||||
private func updateTrialState() {
|
private func updateTrialState() {
|
||||||
let daysSinceInstall = Calendar.current.dateComponents([.day], from: firstLaunchDate, to: Date()).day ?? 0
|
let daysSinceInstall = Calendar.current.dateComponents([.day], from: firstLaunchDate, to: Date()).day ?? 0
|
||||||
let daysRemaining = trialDays - daysSinceInstall
|
let daysRemaining = trialDays - daysSinceInstall
|
||||||
|
AppLogger.iap.debug("updateTrialState: firstLaunchDate=\(self.firstLaunchDate), daysSinceInstall=\(daysSinceInstall), daysRemaining=\(daysRemaining)")
|
||||||
|
|
||||||
if daysRemaining > 0 {
|
if daysRemaining > 0 {
|
||||||
|
AppLogger.iap.debug("updateTrialState: setting .inTrial(daysRemaining: \(daysRemaining))")
|
||||||
state = .inTrial(daysRemaining: daysRemaining)
|
state = .inTrial(daysRemaining: daysRemaining)
|
||||||
} else {
|
} else {
|
||||||
|
AppLogger.iap.debug("updateTrialState: setting .trialExpired")
|
||||||
state = .trialExpired
|
state = .trialExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +512,7 @@ class IAPManager: ObservableObject {
|
|||||||
guard case .verified(let transaction) = result else { continue }
|
guard case .verified(let transaction) = result else { continue }
|
||||||
|
|
||||||
await transaction.finish()
|
await transaction.finish()
|
||||||
await self?.checkSubscriptionStatus()
|
await self?.checkSubscriptionStatus(force: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ struct ReflectSubscriptionStoreView: View {
|
|||||||
case .success(.success(_)):
|
case .success(.success(_)):
|
||||||
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source)
|
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source)
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
await iapManager.checkSubscriptionStatus()
|
await iapManager.checkSubscriptionStatus(force: true)
|
||||||
iapManager.trackSubscriptionAnalytics(source: "purchase_success")
|
iapManager.trackSubscriptionAnalytics(source: "purchase_success")
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
Reference in New Issue
Block a user