|
|
@@ -13,9 +13,13 @@ final class PostGeneratorViewModel {
|
|
|
var showOverwriteConfirmation = false
|
|
|
|
|
|
private var hasPremiumAccess = false
|
|
|
+ private var hasEverPurchasedPremium = false
|
|
|
private var generationService: any PostGenerationServiceProtocol
|
|
|
private let injectedGenerationService: (any PostGenerationServiceProtocol)?
|
|
|
private let historyManager: AIHistoryManager
|
|
|
+ private let freeUsageManager: AIFreeUsageManager
|
|
|
+ var onPaywallRequired: (() -> Void)?
|
|
|
+ private(set) var remainingFreeAIUses: Int
|
|
|
private var imageAccessURL: URL?
|
|
|
private var isAccessingImageResource = false
|
|
|
private let emptyDraft = PostDraft()
|
|
|
@@ -23,22 +27,60 @@ final class PostGeneratorViewModel {
|
|
|
init(
|
|
|
hasPremiumAccess: Bool = false,
|
|
|
generationService: (any PostGenerationServiceProtocol)? = nil,
|
|
|
- historyManager: AIHistoryManager = .shared
|
|
|
+ historyManager: AIHistoryManager = .shared,
|
|
|
+ freeUsageManager: AIFreeUsageManager = .shared
|
|
|
) {
|
|
|
self.hasPremiumAccess = hasPremiumAccess
|
|
|
self.injectedGenerationService = generationService
|
|
|
self.historyManager = historyManager
|
|
|
+ self.freeUsageManager = freeUsageManager
|
|
|
+ self.remainingFreeAIUses = freeUsageManager.remainingUses
|
|
|
+ self.hasEverPurchasedPremium = false
|
|
|
self.generationService = generationService
|
|
|
- ?? PostGenerationServiceFactory.make(hasPremiumAccess: hasPremiumAccess)
|
|
|
+ ?? PostGenerationServiceFactory.make(usesLiveAI: Self.liveAIEnabled(
|
|
|
+ hasPremiumAccess: hasPremiumAccess,
|
|
|
+ hasEverPurchasedPremium: false,
|
|
|
+ freeUsageManager: freeUsageManager
|
|
|
+ ))
|
|
|
}
|
|
|
|
|
|
- func setPremiumAccess(_ hasPremiumAccess: Bool) {
|
|
|
+ func setSubscriptionState(hasPremiumAccess: Bool, hasEverPurchasedPremium: Bool) {
|
|
|
self.hasPremiumAccess = hasPremiumAccess
|
|
|
+ self.hasEverPurchasedPremium = hasEverPurchasedPremium
|
|
|
+ refreshFreeUsageState()
|
|
|
+ }
|
|
|
+
|
|
|
+ func setPremiumAccess(_ hasPremiumAccess: Bool) {
|
|
|
+ setSubscriptionState(
|
|
|
+ hasPremiumAccess: hasPremiumAccess,
|
|
|
+ hasEverPurchasedPremium: hasEverPurchasedPremium
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ func refreshFreeUsageState() {
|
|
|
+ remainingFreeAIUses = freeUsageManager.remainingUses
|
|
|
+ refreshGenerationService()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func refreshGenerationService() {
|
|
|
if injectedGenerationService == nil {
|
|
|
- generationService = PostGenerationServiceFactory.make(hasPremiumAccess: hasPremiumAccess)
|
|
|
+ generationService = PostGenerationServiceFactory.make(usesLiveAI: usesLiveAI)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private static func liveAIEnabled(
|
|
|
+ hasPremiumAccess: Bool,
|
|
|
+ hasEverPurchasedPremium: Bool,
|
|
|
+ freeUsageManager: AIFreeUsageManager
|
|
|
+ ) -> Bool {
|
|
|
+ AIConfiguration.usesLiveAI(
|
|
|
+ hasPremiumAccess: hasPremiumAccess,
|
|
|
+ canUseFreeTrial: freeUsageManager.canUseFreeLiveAI(
|
|
|
+ hasEverPurchasedPremium: hasEverPurchasedPremium
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
var formattedSubreddit: String {
|
|
|
let name = PostDraftValidator.normalizedSubreddit(draft.subreddit)
|
|
|
guard !name.isEmpty else { return "r/subreddit" }
|
|
|
@@ -46,7 +88,27 @@ final class PostGeneratorViewModel {
|
|
|
}
|
|
|
|
|
|
var usesLiveAI: Bool {
|
|
|
- AIConfiguration.usesLiveAI(hasPremiumAccess: hasPremiumAccess)
|
|
|
+ AIConfiguration.usesLiveAI(
|
|
|
+ hasPremiumAccess: hasPremiumAccess,
|
|
|
+ canUseFreeTrial: freeUsageManager.canUseFreeLiveAI(
|
|
|
+ hasEverPurchasedPremium: hasEverPurchasedPremium
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ var generationEngineSubtitle: String {
|
|
|
+ if usesLiveAI {
|
|
|
+ if hasPremiumAccess {
|
|
|
+ return "Create Reddit-ready posts with AI"
|
|
|
+ }
|
|
|
+ let remaining = remainingFreeAIUses
|
|
|
+ let useLabel = remaining == 1 ? "use" : "uses"
|
|
|
+ return "Create Reddit-ready posts with AI (\(remaining) free \(useLabel) left)"
|
|
|
+ }
|
|
|
+ if hasEverPurchasedPremium {
|
|
|
+ return "Upgrade to Premium for unlimited AI generations"
|
|
|
+ }
|
|
|
+ return "Create Reddit-ready posts with AI templates"
|
|
|
}
|
|
|
|
|
|
var hasDraftChanges: Bool {
|
|
|
@@ -127,6 +189,11 @@ final class PostGeneratorViewModel {
|
|
|
func generatePost() async {
|
|
|
guard canGenerate else { return }
|
|
|
|
|
|
+ if requiresPaywallForGeneration() {
|
|
|
+ onPaywallRequired?()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
if needsOverwriteConfirmation {
|
|
|
showOverwriteConfirmation = true
|
|
|
return
|
|
|
@@ -226,16 +293,27 @@ final class PostGeneratorViewModel {
|
|
|
}
|
|
|
|
|
|
private func performGenerate(replaceExisting: Bool) async {
|
|
|
+ if requiresPaywallForGeneration() {
|
|
|
+ onPaywallRequired?()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
isGenerating = true
|
|
|
errorMessage = nil
|
|
|
successMessage = nil
|
|
|
await Task.yield()
|
|
|
|
|
|
+ let usedLiveAI = usesLiveAI
|
|
|
+
|
|
|
do {
|
|
|
let result = try await generationService.generatePost(from: draft)
|
|
|
applyGeneratedPost(result, replaceExisting: replaceExisting)
|
|
|
selectedTab = .preview
|
|
|
- let engine = usesLiveAI ? "AI" : "local AI templates"
|
|
|
+ if usedLiveAI, !hasPremiumAccess, !hasEverPurchasedPremium {
|
|
|
+ freeUsageManager.recordUse(for: .postGenerator)
|
|
|
+ refreshFreeUsageState()
|
|
|
+ }
|
|
|
+ let engine = usedLiveAI ? "AI" : "local AI templates"
|
|
|
successMessage = "Post generated successfully using \(engine)."
|
|
|
let entry = AIHistoryEntry.fromPostGenerator(draft: draft)
|
|
|
Task { @MainActor in
|
|
|
@@ -268,6 +346,10 @@ final class PostGeneratorViewModel {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func requiresPaywallForGeneration() -> Bool {
|
|
|
+ !hasPremiumAccess && (hasEverPurchasedPremium || remainingFreeAIUses == 0)
|
|
|
+ }
|
|
|
+
|
|
|
private func releaseImageAccess() {
|
|
|
if isAccessingImageResource, let imageAccessURL {
|
|
|
imageAccessURL.stopAccessingSecurityScopedResource()
|