Переглянути джерело

Clarify Post Generator compose flow and remove weekly subscription tier.

Post Generator UX:
- Add hasGeneratedContent flag to PostGeneratorViewModel so Compose
  distinguishes configuration inputs from AI output. Set on successful
  Generate and History restore; cleared on Reset.
- Rename the right Compose column from "Post Content" to "Generated
  result" and show an empty-state placeholder until the first generation.
- Split media attachment (image upload, link URL, video URL) into a
  dedicated card that is always visible for those post types, labeled as
  not used by AI.
- Hide title, body, captions, and poll text fields until content is
  generated, then present them as editable AI output with updated copy.
- Add PostGeneratorEmptyResultPlaceholder component for the pre-generate
  dashed-border empty state.

Subscription cleanup:
- Remove the weekly product from paywall.json, SubscriptionProductID,
  PaywallModels, and SubscriptionManager plan ordering so only monthly,
  yearly, and lifetime tiers remain.

Co-authored-by: Cursor <cursoragent@cursor.com>
Uzair Tahir 1 тиждень тому
батько
коміт
b5a3acc265

+ 0 - 1
App AI for Reddit/Managers/SubscriptionManager.swift

@@ -587,7 +587,6 @@ final class SubscriptionManager: ObservableObject {
             SubscriptionProductID.lifetime,
             SubscriptionProductID.yearly,
             SubscriptionProductID.monthly,
-            SubscriptionProductID.weekly,
         ]
 
         func rank(for plan: PaywallPlan) -> Int {

+ 0 - 2
App AI for Reddit/Models/PaywallModels.swift

@@ -48,7 +48,6 @@ struct PaywallPlan: Identifiable, Hashable, Sendable {
         }
 
         switch rawValue {
-        case "weekly": return PaywallPlan(productID: SubscriptionProductID.weekly)
         case "monthly": return PaywallPlan(productID: SubscriptionProductID.monthly)
         case "yearly": return PaywallPlan(productID: SubscriptionProductID.yearly)
         case "lifetime": return PaywallPlan(productID: SubscriptionProductID.lifetime)
@@ -139,7 +138,6 @@ struct PaywallPlan: Identifiable, Hashable, Sendable {
 extension PaywallPlan {
     static func sortOrder(_ lhs: PaywallPlan, _ rhs: PaywallPlan) -> Bool {
         let order = [
-            SubscriptionProductID.weekly,
             SubscriptionProductID.monthly,
             SubscriptionProductID.yearly,
             SubscriptionProductID.lifetime,

+ 1 - 2
App AI for Reddit/Utilities/SubscriptionProductID.swift

@@ -1,12 +1,11 @@
 import Foundation
 
 enum SubscriptionProductID {
-    static let weekly = "com.app_for_reddit.pro.weekly"
     static let monthly = "com.app_for_reddit.pro.monthly"
     static let yearly = "com.app_for_reddit.pro.yearly"
     static let lifetime = "com.app_for_reddit.pro.lifetime"
 
-    static let catalog: [String] = [weekly, monthly, yearly, lifetime]
+    static let catalog: [String] = [monthly, yearly, lifetime]
 
     static func productIDs(from config: PaywallConfig) -> [String] {
         if let ids = config.productIDs, !ids.isEmpty {

+ 7 - 0
App AI for Reddit/ViewModels/PostGeneratorViewModel.swift

@@ -11,6 +11,9 @@ final class PostGeneratorViewModel {
     var successMessage: String?
     var showImageImporter = false
     var showOverwriteConfirmation = false
+    /// True after a successful Generate or History restore. Cleared on Reset.
+    /// Controls whether title/body result fields are shown in Compose.
+    private(set) var hasGeneratedContent = false
 
     private var hasPremiumAccess = false
     private var hasEverPurchasedPremium = false
@@ -228,6 +231,7 @@ final class PostGeneratorViewModel {
     func resetDraft() {
         releaseImageAccess()
         draft = PostDraft()
+        hasGeneratedContent = false
         selectedTab = .compose
         clearMessages()
     }
@@ -236,6 +240,7 @@ final class PostGeneratorViewModel {
         guard case .postGenerator(let storedDraft) = entry.payload else { return }
         releaseImageAccess()
         draft = storedDraft.postDraft
+        hasGeneratedContent = true
         selectedTab = .preview
         clearMessages()
         successMessage = "Restored from history."
@@ -344,6 +349,8 @@ final class PostGeneratorViewModel {
                 draft.pollOptions = options.map { PollOption(text: $0) }
             }
         }
+
+        hasGeneratedContent = true
     }
 
     private func requiresPaywallForGeneration() -> Bool {

+ 35 - 0
App AI for Reddit/Views/Components/PostGeneratorComponents.swift

@@ -288,6 +288,41 @@ struct PostGeneratorMessageBanner: View {
     }
 }
 
+struct PostGeneratorEmptyResultPlaceholder: View {
+    var body: some View {
+        VStack(spacing: 12) {
+            Image(systemName: "sparkles")
+                .font(.system(size: 28, weight: .medium))
+                .foregroundStyle(AppTheme.accentPurpleLight)
+
+            VStack(spacing: 6) {
+                Text("Fill Configuration, then press Generate")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(AppTheme.textPrimary)
+                    .multilineTextAlignment(.center)
+
+                Text("AI writes the title and body from your Configuration. You don't need to fill anything here first.")
+                    .font(.system(size: 11))
+                    .foregroundStyle(AppTheme.textSecondary)
+                    .multilineTextAlignment(.center)
+                    .fixedSize(horizontal: false, vertical: true)
+            }
+        }
+        .padding(.horizontal, 24)
+        .padding(.vertical, 36)
+        .frame(maxWidth: .infinity, minHeight: 180)
+        .background(AppTheme.panelBackground)
+        .clipShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall))
+        .overlay(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .strokeBorder(
+                    AppTheme.accentPurple.opacity(0.3),
+                    style: StrokeStyle(lineWidth: 1, dash: [6, 4])
+                )
+        )
+    }
+}
+
 // MARK: - Markdown
 
 struct PostMarkdownText: View {

+ 92 - 68
App AI for Reddit/Views/PostGeneratorView.swift

@@ -194,7 +194,7 @@ struct PostGeneratorView: View {
                 .frame(width: 1)
                 .padding(.vertical, 2)
 
-            composeColumn(title: "Post Content", icon: "doc.text") {
+            composeColumn(title: "Generated result", icon: "doc.badge.sparkles") {
                 editorPanel
             }
             .frame(minWidth: 0, maxWidth: .infinity)
@@ -310,119 +310,143 @@ struct PostGeneratorView: View {
 
     private var editorPanel: some View {
         VStack(spacing: Layout.sectionSpacing) {
-            PostFormCard(title: "Title", subtitle: "Filled by AI after generating, or write your own") {
+            mediaAttachmentSection
+
+            if viewModel.hasGeneratedContent {
+                generatedContentSection
+            } else {
+                PostGeneratorEmptyResultPlaceholder()
+            }
+        }
+    }
+
+    @ViewBuilder
+    private var mediaAttachmentSection: some View {
+        switch viewModel.draft.postType {
+        case .image:
+            PostFormCard(title: "Media", subtitle: "Attach an image for your post — not used by AI") {
+                imageUploadArea
+            }
+        case .link:
+            PostFormCard(title: "Media", subtitle: "Attach the URL you'll share — not used by AI") {
+                VStack(alignment: .leading, spacing: 6) {
+                    PostFormField(
+                        label: "URL",
+                        placeholder: "https://example.com/article",
+                        text: $viewModel.draft.linkURL
+                    )
+
+                    if !viewModel.draft.linkURL.isEmpty,
+                       !PostDraftValidator.isValidHTTPURL(viewModel.draft.linkURL) {
+                        validationHint("Enter a valid http or https URL.")
+                    }
+                }
+            }
+        case .video:
+            PostFormCard(title: "Media", subtitle: "Attach a YouTube, Vimeo, or video URL — not used by AI") {
+                VStack(alignment: .leading, spacing: 6) {
+                    PostFormField(
+                        label: "Video URL",
+                        placeholder: "https://youtube.com/watch?v=…",
+                        text: $viewModel.draft.videoURL
+                    )
+
+                    if !viewModel.draft.videoURL.isEmpty,
+                       !PostDraftValidator.isValidHTTPURL(viewModel.draft.videoURL) {
+                        validationHint("Enter a valid http or https URL.")
+                    }
+                }
+            }
+        case .text, .poll:
+            EmptyView()
+        }
+    }
+
+    private var generatedContentSection: some View {
+        VStack(spacing: Layout.sectionSpacing) {
+            PostFormCard(title: "Title", subtitle: "Generated by AI — edit if you like") {
                 PostFormField(
                     label: "Post Title",
-                    placeholder: "An engaging title for your post",
+                    placeholder: "Generated title appears here",
                     text: $viewModel.draft.title
                 )
             }
 
-            postTypeEditor
-                .frame(minHeight: 260, alignment: .top)
+            generatedPostTypeEditor
+                .frame(minHeight: 160, alignment: .top)
 
             characterCountFooter
         }
     }
 
     @ViewBuilder
-    private var postTypeEditor: some View {
+    private var generatedPostTypeEditor: some View {
         switch viewModel.draft.postType {
         case .text:
             textPostEditor
         case .image:
-            imagePostEditor
+            imageCaptionEditor
         case .link:
-            linkPostEditor
+            linkDescriptionEditor
         case .video:
-            videoPostEditor
+            videoDescriptionEditor
         case .poll:
             pollPostEditor
         }
     }
 
     private var textPostEditor: some View {
-        PostFormCard(title: "Body", subtitle: "Markdown supported — **bold**, *italic*, links") {
+        PostFormCard(title: "Body", subtitle: "Generated by AI — markdown supported") {
             PostFormTextEditor(
                 label: "Post Body",
-                placeholder: "Write your post content here…",
+                placeholder: "Edit the generated body…",
                 text: $viewModel.draft.body,
                 minHeight: 220
             )
         }
     }
 
-    private var imagePostEditor: some View {
-        PostFormCard(title: "Image", subtitle: "Upload an image for your post") {
-            VStack(alignment: .leading, spacing: 12) {
-                imageUploadArea
-
-                PostFormTextEditor(
-                    label: "Caption (optional)",
-                    placeholder: "Add context for your image…",
-                    text: $viewModel.draft.body,
-                    minHeight: 80
-                )
-            }
+    private var imageCaptionEditor: some View {
+        PostFormCard(title: "Caption", subtitle: "Generated by AI — edit if you like") {
+            PostFormTextEditor(
+                label: "Caption (optional)",
+                placeholder: "Edit the generated caption…",
+                text: $viewModel.draft.body,
+                minHeight: 80
+            )
         }
     }
 
-    private var linkPostEditor: some View {
-        PostFormCard(title: "Link", subtitle: "Share a URL with the community") {
-            VStack(alignment: .leading, spacing: 12) {
-                PostFormField(
-                    label: "URL",
-                    placeholder: "https://example.com/article",
-                    text: $viewModel.draft.linkURL
-                )
-
-                if !viewModel.draft.linkURL.isEmpty,
-                   !PostDraftValidator.isValidHTTPURL(viewModel.draft.linkURL) {
-                    validationHint("Enter a valid http or https URL.")
-                }
-
-                PostFormTextEditor(
-                    label: "Description (optional)",
-                    placeholder: "Why are you sharing this link?",
-                    text: $viewModel.draft.body,
-                    minHeight: 100
-                )
-            }
+    private var linkDescriptionEditor: some View {
+        PostFormCard(title: "Description", subtitle: "Generated by AI — edit if you like") {
+            PostFormTextEditor(
+                label: "Description (optional)",
+                placeholder: "Edit the generated description…",
+                text: $viewModel.draft.body,
+                minHeight: 100
+            )
         }
     }
 
-    private var videoPostEditor: some View {
-        PostFormCard(title: "Video", subtitle: "Link to YouTube, Vimeo, or direct video URL") {
-            VStack(alignment: .leading, spacing: 12) {
-                PostFormField(
-                    label: "Video URL",
-                    placeholder: "https://youtube.com/watch?v=…",
-                    text: $viewModel.draft.videoURL
-                )
-
-                if !viewModel.draft.videoURL.isEmpty,
-                   !PostDraftValidator.isValidHTTPURL(viewModel.draft.videoURL) {
-                    validationHint("Enter a valid http or https URL.")
-                }
-
-                PostFormTextEditor(
-                    label: "Description (optional)",
-                    placeholder: "Add context for your video…",
-                    text: $viewModel.draft.body,
-                    minHeight: 100
-                )
-            }
+    private var videoDescriptionEditor: some View {
+        PostFormCard(title: "Description", subtitle: "Generated by AI — edit if you like") {
+            PostFormTextEditor(
+                label: "Description (optional)",
+                placeholder: "Edit the generated description…",
+                text: $viewModel.draft.body,
+                minHeight: 100
+            )
         }
     }
 
     private var pollPostEditor: some View {
-        PostFormCard(title: "Poll", subtitle: "2–6 options, community votes") {
+        PostFormCard(title: "Poll", subtitle: "Generated options — edit if you like") {
             VStack(alignment: .leading, spacing: 12) {
                 ForEach(Array(viewModel.draft.pollOptions.enumerated()), id: \.element.id) { index, option in
                     HStack(spacing: 8) {
                         PostFormField(
                             label: "Option \(index + 1)",
-                            placeholder: "Enter poll option",
+                            placeholder: "Poll option",
                             text: Binding(
                                 get: { option.text },
                                 set: { viewModel.updatePollOption(id: option.id, text: $0) }
@@ -495,7 +519,7 @@ struct PostGeneratorView: View {
 
                 PostFormTextEditor(
                     label: "Context (optional)",
-                    placeholder: "Explain why you're running this poll…",
+                    placeholder: "Edit the generated poll context…",
                     text: $viewModel.draft.body,
                     minHeight: 80
                 )

+ 0 - 14
App AI for Reddit/paywall.json

@@ -1,6 +1,5 @@
 {
   "productIDs": [
-    "com.app_for_reddit.pro.weekly",
     "com.app_for_reddit.pro.monthly",
     "com.app_for_reddit.pro.yearly",
     "com.app_for_reddit.pro.lifetime"
@@ -44,19 +43,6 @@
     "subtitle": "Pay once, no recurring charges."
   },
   "plans": {
-    "weekly": {
-      "title": "Weekly",
-      "subtitleTemplate": "{price} / week, cancel anytime",
-      "ctaTemplate": "Get Premium for {price} / Week",
-      "fallbackPrice": "$2.99",
-      "priceSuffix": "/wk",
-      "fallbackBillingDescription": "Billed at {price} every week",
-      "features": [
-        "Unlimited AI Generations",
-        "Post Generator & Title Optimizer",
-        "Comment Writer with smart replies"
-      ]
-    },
     "monthly": {
       "title": "Monthly",
       "subtitleTemplate": "{price} / month, cancel anytime",