Add debug bypass subscription toggle, tests, and data layer improvements
- Add runtime toggle in Settings (DEBUG only) to bypass subscription/hide trial banner - IAPManager.bypassSubscription is now a @Published var persisted via UserDefaults - Hide upgrade banner in SettingsTabView and trial warnings when bypass is enabled - Add FeelsTests directory with integration tests - Update DataController, DataControllerGET, DataControllerUPDATE - Update Xcode project and scheme configuration - Update localization strings and App Store screen docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,11 +36,13 @@ class IAPManager: ObservableObject {
|
||||
// MARK: - Debug Toggle
|
||||
|
||||
/// Set to `true` to bypass all subscription checks and grant full access (for development only)
|
||||
/// Set to `false` to test trial/subscription behavior in DEBUG builds
|
||||
/// Togglable at runtime in DEBUG builds via Settings > Debug > Bypass Subscription
|
||||
#if DEBUG
|
||||
static let bypassSubscription = false
|
||||
@Published var bypassSubscription: Bool {
|
||||
didSet { UserDefaults.standard.set(bypassSubscription, forKey: "debug_bypassSubscription") }
|
||||
}
|
||||
#else
|
||||
static let bypassSubscription = false
|
||||
let bypassSubscription = false
|
||||
#endif
|
||||
|
||||
// MARK: - Constants
|
||||
@@ -96,7 +98,7 @@ class IAPManager: ObservableObject {
|
||||
}
|
||||
|
||||
var hasFullAccess: Bool {
|
||||
if Self.bypassSubscription { return true }
|
||||
if bypassSubscription { return true }
|
||||
switch state {
|
||||
case .subscribed, .billingRetry, .gracePeriod, .inTrial:
|
||||
return true
|
||||
@@ -106,7 +108,7 @@ class IAPManager: ObservableObject {
|
||||
}
|
||||
|
||||
var shouldShowPaywall: Bool {
|
||||
if Self.bypassSubscription { return false }
|
||||
if bypassSubscription { return false }
|
||||
switch state {
|
||||
case .trialExpired, .expired, .revoked:
|
||||
return true
|
||||
@@ -116,6 +118,7 @@ class IAPManager: ObservableObject {
|
||||
}
|
||||
|
||||
var shouldShowTrialWarning: Bool {
|
||||
if bypassSubscription { return false }
|
||||
if case .inTrial = state { return true }
|
||||
return false
|
||||
}
|
||||
@@ -137,6 +140,9 @@ class IAPManager: ObservableObject {
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
#if DEBUG
|
||||
self.bypassSubscription = UserDefaults.standard.bool(forKey: "debug_bypassSubscription")
|
||||
#endif
|
||||
restoreCachedSubscriptionState()
|
||||
updateListenerTask = listenForTransactions()
|
||||
|
||||
@@ -219,7 +225,7 @@ class IAPManager: ObservableObject {
|
||||
|
||||
/// Sync subscription status to UserDefaults for widget access
|
||||
private func syncSubscriptionStatusToUserDefaults() {
|
||||
let accessValue = Self.bypassSubscription ? true : hasFullAccess
|
||||
let accessValue = bypassSubscription ? true : hasFullAccess
|
||||
GroupUserDefaults.groupDefaults.set(accessValue, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
||||
}
|
||||
|
||||
@@ -373,7 +379,7 @@ class IAPManager: ObservableObject {
|
||||
case .unknown:
|
||||
status = "unknown"
|
||||
isSubscribed = false
|
||||
hasFullAccess = Self.bypassSubscription
|
||||
hasFullAccess = bypassSubscription
|
||||
willAutoRenew = nil
|
||||
isInGracePeriod = nil
|
||||
trialDaysRemaining = nil
|
||||
|
||||
@@ -41,8 +41,8 @@ final class DataController: ObservableObject {
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
private init() {
|
||||
container = SharedModelContainer.createWithFallback(useCloudKit: true)
|
||||
init(container: ModelContainer? = nil) {
|
||||
self.container = container ?? SharedModelContainer.createWithFallback(useCloudKit: true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ extension DataController {
|
||||
|
||||
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||
predicate: #Predicate { entry in
|
||||
entry.forDate >= startDate && entry.forDate <= endDate
|
||||
entry.forDate >= startDate && entry.forDate < endDate
|
||||
},
|
||||
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||
)
|
||||
@@ -66,7 +66,8 @@ extension DataController {
|
||||
return (0, nil)
|
||||
}
|
||||
|
||||
let entries = getData(startDate: yearAgo, endDate: votingDate, includedDays: [])
|
||||
let endOfVotingDay = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? votingDate
|
||||
let entries = getData(startDate: yearAgo, endDate: endOfVotingDay, includedDays: [])
|
||||
.filter { $0.mood != .missing && $0.mood != .placeholder }
|
||||
|
||||
guard !entries.isEmpty else { return (0, nil) }
|
||||
|
||||
@@ -16,6 +16,7 @@ extension DataController {
|
||||
}
|
||||
|
||||
entry.moodValue = mood.rawValue
|
||||
entry.timestamp = Date()
|
||||
saveAndRunDataListeners()
|
||||
|
||||
AnalyticsManager.shared.track(.moodUpdated(mood: mood.rawValue))
|
||||
|
||||
@@ -36,7 +36,7 @@ struct SettingsTabView: View {
|
||||
.padding(.top, 8)
|
||||
|
||||
// Upgrade Banner (only show if not subscribed)
|
||||
if !iapManager.isSubscribed {
|
||||
if !iapManager.isSubscribed && !iapManager.bypassSubscription {
|
||||
UpgradeBannerView(
|
||||
showWhyUpgrade: $showWhyUpgrade,
|
||||
showSubscriptionStore: $showSubscriptionStore,
|
||||
|
||||
@@ -67,6 +67,7 @@ struct SettingsContentView: View {
|
||||
#if DEBUG
|
||||
// Debug section
|
||||
debugSectionHeader
|
||||
bypassSubscriptionToggle
|
||||
trialDateButton
|
||||
animationLabButton
|
||||
paywallPreviewButton
|
||||
@@ -211,6 +212,35 @@ struct SettingsContentView: View {
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
private var bypassSubscriptionToggle: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "lock.open.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Bypass Subscription")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Hide trial banner & grant full access")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: $iapManager.bypassSubscription)
|
||||
.labelsHidden()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var trialDateButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
@@ -1282,6 +1312,7 @@ struct SettingsView: View {
|
||||
Group {
|
||||
Divider()
|
||||
Text("Test builds only")
|
||||
Toggle("Bypass Subscription", isOn: $iapManager.bypassSubscription)
|
||||
addTestDataCell
|
||||
clearDB
|
||||
// fixWeekday
|
||||
|
||||
Reference in New Issue
Block a user