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:
Trey t
2026-02-15 17:12:56 -06:00
parent 7c142568be
commit 7639f881da
14 changed files with 1064 additions and 34 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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) }

View File

@@ -16,6 +16,7 @@ extension DataController {
}
entry.moodValue = mood.rawValue
entry.timestamp = Date()
saveAndRunDataListeners()
AnalyticsManager.shared.track(.moodUpdated(mood: mood.rawValue))

View File

@@ -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,

View File

@@ -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