Add PremiumGate utility for generic freemium settings gating

- Add PremiumGate enum with get/canSet helpers for premium-gated settings
- Supports entirely premium settings (returns default for free users)
- Supports partial premium (only specific values require premium)
- Preserves stored values so re-subscribing restores previous choices
This commit is contained in:
Matt Bruce 2026-01-04 13:22:57 -06:00
parent 40d1285c38
commit b5a5c62fa8
2 changed files with 118 additions and 1 deletions

View File

@ -0,0 +1,108 @@
//
// PremiumGate.swift
// Bedrock
//
// Utility for premium-gating settings values in freemium apps.
// Provides a consistent pattern for handling premium vs free user access.
//
import Foundation
/// Utility enum for premium-gating settings values.
///
/// Use this to create consistent premium/free behavior across your app's settings.
/// Values are preserved in storage but defaults are returned for non-premium users,
/// so users don't lose their settings if they unsubscribe and later re-subscribe.
///
/// ## Usage
///
/// For boolean settings that are entirely premium:
/// ```swift
/// var isSkinSmoothingEnabled: Bool {
/// get { PremiumGate.get(cloudSync.data.isSkinSmoothingEnabled, default: false, isPremium: isPremiumUnlocked) }
/// set { guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
/// updateSettings { $0.isSkinSmoothingEnabled = newValue }
/// }
/// }
/// ```
///
/// For settings where only some values are premium:
/// ```swift
/// var selectedTimer: TimerOption {
/// get {
/// let stored = TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .three
/// return PremiumGate.get(stored, default: .three, premiumValues: [.five, .ten], isPremium: isPremiumUnlocked)
/// }
/// set {
/// guard PremiumGate.canSet(newValue, premiumValues: [.five, .ten], isPremium: isPremiumUnlocked) else { return }
/// updateSettings { $0.selectedTimerRaw = newValue.rawValue }
/// }
/// }
/// ```
public enum PremiumGate {
// MARK: - Getters
/// Returns the stored value if premium, otherwise returns the default.
///
/// Use for settings that are entirely premium-only (e.g., skin smoothing, mirror flip).
/// - Parameters:
/// - stored: The value stored in settings
/// - defaultValue: The value to return for non-premium users
/// - isPremium: Whether the user has premium access
/// - Returns: The stored value if premium, otherwise the default
public static func get<T>(
_ stored: T,
default defaultValue: T,
isPremium: Bool
) -> T {
isPremium ? stored : defaultValue
}
/// Returns the stored value if premium or if it's not a premium value, otherwise returns the default.
///
/// Use for settings where only some values are premium (e.g., timer options, quality levels).
/// - Parameters:
/// - stored: The value stored in settings
/// - defaultValue: The value to return for non-premium users when they have a premium value stored
/// - premiumValues: The set of values that require premium access
/// - isPremium: Whether the user has premium access
/// - Returns: The stored value if premium or not a premium value, otherwise the default
public static func get<T: Hashable>(
_ stored: T,
default defaultValue: T,
premiumValues: Set<T>,
isPremium: Bool
) -> T {
if !isPremium && premiumValues.contains(stored) {
return defaultValue
}
return stored
}
// MARK: - Setters
/// Checks if setting any value should be allowed (for entirely premium settings).
///
/// - Parameter isPremium: Whether the user has premium access
/// - Returns: True if the user can modify this setting
public static func canSet(isPremium: Bool) -> Bool {
isPremium
}
/// Checks if setting a specific value should be allowed.
///
/// Use for settings where only some values are premium.
/// - Parameters:
/// - value: The value the user wants to set
/// - premiumValues: The set of values that require premium access
/// - isPremium: Whether the user has premium access
/// - Returns: True if the user can set this specific value
public static func canSet<T: Hashable>(
_ value: T,
premiumValues: Set<T>,
isPremium: Bool
) -> Bool {
isPremium || !premiumValues.contains(value)
}
}

View File

@ -1 +1,10 @@
{"sourceLanguage":"en","strings":{},"version":"1.0"}
{
"sourceLanguage" : "en",
"strings" : {
"%lld%%" : {
"comment" : "A text label showing the current volume percentage. The argument is the volume as a percentage (0.0 to 1.0).",
"isCommentAutoGenerated" : true
}
},
"version" : "1.1"
}