Browse Source

Improve App Store compliance and defer paywall prices until StoreKit loads.

Add App Store Connect privacy and age-rating guides, set the privacy policy URL and social networking category, update the children's privacy policy to 17+, and show "Loading…" on paywall cards instead of hardcoded fallback prices.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 tuần trước cách đây
mục cha
commit
70832da2d9

+ 4 - 2
App AI for Reddit.xcodeproj/project.pbxproj

@@ -258,8 +258,9 @@
 				INFOPLIST_KEY_CFBundleDisplayName = "App AI for Reddit";
 				INFOPLIST_KEY_CFBundleName = "App AI for Reddit";
 				INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
-				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 App AI for Reddit";
+				INFOPLIST_KEY_NSPrivacyPolicyURL = "https://sites.google.com/view/appforreddit/privacy-policy";
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -293,8 +294,9 @@
 				INFOPLIST_KEY_CFBundleDisplayName = "App AI for Reddit";
 				INFOPLIST_KEY_CFBundleName = "App AI for Reddit";
 				INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
-				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 App AI for Reddit";
+				INFOPLIST_KEY_NSPrivacyPolicyURL = "https://sites.google.com/view/appforreddit/privacy-policy";
 				INFOPLIST_KEY_NSPrincipalClass = NSApplication;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",

+ 2 - 2
App AI for Reddit/Managers/SubscriptionManager.swift

@@ -174,7 +174,7 @@ final class SubscriptionManager: ObservableObject {
 
     func mainPrice(for plan: PaywallPlan) -> String {
         let config = PaywallConfigService.shared.config
-        return product(for: plan)?.displayPrice ?? plan.planCopy(from: config).fallbackPrice
+        return product(for: plan)?.displayPrice ?? config.loadingPrice
     }
 
     func priceSuffix(for plan: PaywallPlan) -> String? {
@@ -184,7 +184,7 @@ final class SubscriptionManager: ObservableObject {
     func billingDescription(for plan: PaywallPlan) -> String {
         let config = PaywallConfigService.shared.config
         guard let product = product(for: plan) else {
-            return plan.fallbackBillingDescription(config: config)
+            return config.loadingDisclosure
         }
 
         switch plan {

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

@@ -53,7 +53,7 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
     }
 
     func localizedPrice(from product: Product?, config: PaywallConfig) -> String {
-        product?.displayPrice ?? planCopy(from: config).fallbackPrice
+        product?.displayPrice ?? config.loadingPrice
     }
 
     func localizedSubtitle(
@@ -61,7 +61,7 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
         config: PaywallConfig,
         trial: PaywallTrialDisplay? = nil
     ) -> String {
-        guard let product else { return config.loadingPrice }
+        guard let product else { return "" }
         let copy = planCopy(from: config)
         if let trial, let template = copy.trialSubtitleTemplate {
             return template

+ 9 - 7
App AI for Reddit/Views/PaywallView.swift

@@ -442,19 +442,21 @@ private struct PaywallPricingCard: View {
                     .font(.system(size: metrics.ctaFontSize, weight: .semibold))
                     .foregroundStyle(AppTheme.textPrimary)
 
-                Text(plan.localizedSubtitle(from: product, config: config, trial: trial))
-                    .font(.system(size: metrics.featureFontSize * 0.85))
-                    .foregroundStyle(AppTheme.textTertiary)
-                    .fixedSize(horizontal: false, vertical: true)
-                    .padding(.top, 2)
+                if product != nil {
+                    Text(plan.localizedSubtitle(from: product, config: config, trial: trial))
+                        .font(.system(size: metrics.featureFontSize * 0.85))
+                        .foregroundStyle(AppTheme.textTertiary)
+                        .fixedSize(horizontal: false, vertical: true)
+                        .padding(.top, 2)
+                }
 
                 HStack(alignment: .firstTextBaseline, spacing: 2) {
                     Text(plan.localizedPrice(from: product, config: config))
                         .font(.system(size: metrics.titleFontSize * 0.85, weight: .bold))
                         .foregroundStyle(AppTheme.textPrimary)
 
-                    if planCopy.priceSuffix != nil {
-                        Text(planCopy.priceSuffix ?? "")
+                    if product != nil, let priceSuffix = planCopy.priceSuffix {
+                        Text(priceSuffix)
                             .font(.system(size: metrics.featureFontSize))
                             .foregroundStyle(AppTheme.textSecondary)
                     }

+ 1 - 1
App AI for Reddit/paywall.json

@@ -143,7 +143,7 @@
     "manageSubscriptionAction": "Manage Subscription",
     "lifetimeAction": "Manage Subscription"
   },
-  "loadingPrice": "",
+  "loadingPrice": "Loading…",
   "loadingCTA": "Loading plans…",
   "subscriptionDisclosureTemplate": "Payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",
   "trialDisclosureTemplate": "After your {trial} free trial, payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",

+ 35 - 0
AppStoreConnect/AgeRatingQuestionnaire.md

@@ -0,0 +1,35 @@
+# App Store Connect — Age Rating Questionnaire
+
+This app embeds Reddit (user-generated content + unrestricted web access) and supports NSFW post drafting. **Underrating is a common rejection reason for Reddit-related apps.** Target rating: **17+**.
+
+Complete the questionnaire in **App Store Connect → App Information → Age Rating**.
+
+## Required overrides (answer honestly)
+
+| Content descriptor | Recommended answer | Reason |
+|--------------------|-------------------|--------|
+| **Unrestricted Web Access** | **Yes** | Premium users browse `reddit.com` in an embedded web view with full site navigation. |
+| **User-Generated Content** | **Yes** | Reddit posts, comments, and media are user-generated. AI tools also produce user-directed draft content. |
+| **Mature/Suggestive Themes** | **Frequent/Intense** or **Infrequent/Mild** (minimum: Infrequent/Mild) | Reddit includes mature communities; the app can tag drafts as NSFW. |
+| **Sexual Content or Nudity** | **Infrequent/Mild** or higher | Reddit may display sexual content; NSFW drafting is supported. |
+| **Profanity or Crude Humor** | **Infrequent/Mild** or **Frequent/Intense** | Common on Reddit. |
+| **Realistic Violence** | **Infrequent/Mild** | Possible via Reddit UGC. |
+| **Horror/Fear Themes** | **Infrequent/Mild** | Possible via Reddit UGC. |
+| **Alcohol, Tobacco, or Drug Use** | **Infrequent/Mild** | Possible via Reddit UGC. |
+| **Medical/Treatment Information** | **None** or **Infrequent/Mild** | Only if shown on Reddit threads users open. |
+| **Gambling and Contests** | **None** | App does not include gambling. |
+| **Simulated Gambling** | **None** | — |
+| **Cartoon or Fantasy Violence** | **None** or **Infrequent/Mild** | — |
+| **Guns or Other Weapons** | **None** or **Infrequent/Mild** | Possible via Reddit UGC. |
+
+## Expected outcome
+
+With **Unrestricted Web Access** and **User-Generated Content** enabled, Apple typically assigns **17+**.
+
+Do **not** submit with **4+**, **9+**, or **12+** unless Apple’s computed rating still lands at 17+ after your answers.
+
+## App Store metadata alignment
+
+- **Category:** Social Networking (matches `LSApplicationCategoryType` in the project).
+- **App description:** Mention that the app provides access to Reddit user-generated content and is intended for users **17 and older**.
+- **Screenshots:** Avoid NSFW or profane content in marketing assets even if the app can access such material in-product.

+ 65 - 0
AppStoreConnect/AppPrivacyQuestionnaire.md

@@ -0,0 +1,65 @@
+# App Store Connect — App Privacy Questionnaire
+
+Use these answers in **App Store Connect → App Privacy**. They must match `PrivacyInfo.xcprivacy` and the live app behavior.
+
+> **Do not select “Data Not Collected.”** The app collects user-entered text for AI features and shares it with OpenAI when live generation runs.
+
+## Summary
+
+| Question | Answer |
+|----------|--------|
+| Do you or your third-party partners collect data from this app? | **Yes** |
+| Is data used to track users? | **No** |
+| Is data linked to the user’s identity? | **No** |
+
+## Data type: Other User Content
+
+Covers text the user enters in Post Generator, Title Optimizer, and Comment Writer (topics, titles, drafts, subreddit names, post URLs, parent comments, tone preferences, NSFW/spoiler tags) and AI responses saved locally in AI History.
+
+### For each purpose, answer:
+
+| Field | Value |
+|-------|-------|
+| Data type | **Other User Content** |
+| Collected | **Yes** |
+| Linked to identity | **No** |
+| Used for tracking | **No** |
+| Purpose | **App Functionality** |
+
+### Third-party sharing (critical)
+
+When App Store Connect asks whether this data is shared with third parties:
+
+| Third party | Shared? | Why |
+|-------------|---------|-----|
+| **OpenAI** (`api.openai.com`) | **Yes** | Live AI generation sends the user’s prompt text to OpenAI’s API. Applies to premium users and free-trial generations (up to 3 total). |
+| Apple (StoreKit) | No for this data type | Subscription checks do not send AI inputs to Apple. |
+| Reddit | No for this data type | Embedded Reddit browsing is direct user ↔ Reddit interaction; the app does not extract or transmit Reddit browsing data to its own servers. |
+
+For **OpenAI** sharing details:
+
+- **Shared for App Functionality** — generating post titles, bodies, optimized titles, and comments.
+- **Not used for tracking or advertising.**
+- **Not linked to user identity** — prompts are not accompanied by name, email, Apple ID, or a persistent user identifier.
+
+## What is NOT collected by the app
+
+Declare **only** what applies. Do **not** add these unless behavior changes:
+
+- Contact Info, Identifiers, Location, Browsing History, Purchases, Diagnostics, Usage Data, Sensitive Info
+- No third-party analytics SDKs (Amplitude, Firebase, etc.)
+- No advertising networks
+
+## After uploading a build
+
+1. Open **App Privacy** in App Store Connect.
+2. Confirm the privacy manifest from the build populated **Other User Content**.
+3. If answers were previously set to “Data Not Collected,” reset and re-answer using this guide.
+4. Under **Other User Content → App Functionality**, enable **third-party sharing** and list processing by OpenAI.
+5. Save and verify the App Privacy label preview shows **Data Not Linked to You → Other User Content**.
+
+## Privacy policy URL
+
+Set in App Store Connect and in the app Info.plist:
+
+`https://sites.google.com/view/appforreddit/privacy-policy`

+ 18 - 0
AppStoreConnect/README.md

@@ -0,0 +1,18 @@
+# App Store Connect Submission Notes
+
+Reference guides for App Review compliance. Complete these in App Store Connect before submitting.
+
+| Guide | Purpose |
+|-------|---------|
+| [AppPrivacyQuestionnaire.md](AppPrivacyQuestionnaire.md) | Privacy nutrition labels — must declare **Other User Content** shared with **OpenAI** |
+| [AgeRatingQuestionnaire.md](AgeRatingQuestionnaire.md) | Age rating — target **17+** for Reddit UGC and unrestricted web access |
+
+## Checklist before submit
+
+- [ ] App Privacy is **not** set to “Data Not Collected”
+- [ ] **Other User Content** → App Functionality → shared with **OpenAI**
+- [ ] Age rating questionnaire includes **Unrestricted Web Access** and **User-Generated Content**
+- [ ] Computed age rating is **17+**
+- [ ] Privacy Policy URL matches the app: `https://sites.google.com/view/appforreddit/privacy-policy`
+- [ ] App category is **Social Networking**
+- [ ] `PrivacyInfo.xcprivacy` is included in the app bundle (auto-included from the target folder)

+ 1 - 1
website/privacy/index.html

@@ -228,7 +228,7 @@
 
       <section class="legal-section">
         <h2>10. Children&apos;s Privacy</h2>
-        <p>The app is not directed to children under 13, or the minimum age required in your jurisdiction. We do not knowingly collect personal information from children.</p>
+        <p>The app is intended for users <strong>17 years of age or older</strong> because it provides access to Reddit user-generated content and tools that can draft mature (NSFW) posts. It is not directed to children. We do not knowingly collect personal information from anyone under 17.</p>
       </section>
 
       <section class="legal-section">