StoreKit

How I Fix Guideline 3.1.2(c) When Subscription Setup Passes but Value Communication Fails

A Guideline 3.1.2(c) rejection analysis: subscription code can pass while value communication still fails App Review.

2026-04-18SubscriptionsStoreKitShould I DecideIAPiOS

In the first App Review pass for Should I Decide, the binary and StoreKit products were both valid, but the reviewer still asked what paid users actually unlock beyond a generic "premium" label. That feedback exposed an architecture gap, not a payment bug.

This pattern appears frequently in developer forum threads under Guideline 3.1.2(c): auto-renewing subscriptions are technically configured, but users cannot clearly see title, duration, price semantics, and concrete paid capability before subscribing.

The app may be useful every day, but each individual interaction is tiny. A decision-support app like Should I Decide might help a user resolve a small choice in ten seconds. That is exactly the value: fast cognitive relief. It is also why the subscription architecture needs to be very clear. The product cannot hide behind complexity.

For this type of app, StoreKit integration is only one layer. The deeper architecture is entitlement, usage policy, feature segmentation, offline behavior, and recovery from StoreKit failure.

If the app cannot explain in code what the free user gets, what the subscriber gets, and how access behaves when the network is bad, the paywall will feel arbitrary.

SwiftUI EntitlementStore cached access StoreKit transactions UsagePolicy free / plus AI Engine mode limits
Figure 1: The paywall is only the visible layer. The product needs an entitlement and usage architecture that remains coherent when StoreKit is slow or unavailable.

Subscription Rejection Trigger Matrix (Utility Apps)

Common trigger in utility appsGuideline pressureHow I resolve it
Paid tier is labeled "premium" without concrete capability boundaries3.1.2(c)Encode free/paid limits as explicit usage policy in-product
Price terms or renewal context are hard to verify before purchase3.1.2Make subscription title, duration, and links immediately visible
Paywall appears before user sees free value loop3.1.2 UX expectationShow baseline utility first, then upgrade path
Registration is forced for non-account subscription actions5.1.1Keep registration optional unless account scope is truly required

Define value as usage policy, not marketing text

For Should I Decide, the recurring value is repeated decision support:

  1. More daily decision sessions.
  2. Saved decision history.
  3. Deeper comparison prompts.
  4. More context retained across repeated dilemmas.

That should exist as code, not only paywall copy.

enum AccessTier: Equatable {
    case free
    case plus(expirationDate: Date?)
}

struct DecisionUsagePolicy {
    let tier: AccessTier

    var dailySessionLimit: Int {
        switch tier {
        case .free: return 5
        case .plus: return 100
        }
    }

    var allowsHistory: Bool {
        if case .plus = tier { return true }
        return false
    }

    var allowsDeepComparison: Bool {
        if case .plus = tier { return true }
        return false
    }
}

The paywall can then describe actual behavior. "Premium AI" is vague. "More daily decision sessions and saved history" is testable.

The entitlement store

I keep StoreKit transaction handling away from views:

@MainActor
final class EntitlementStore: ObservableObject {
    @Published private(set) var tier: AccessTier = .free
    @Published private(set) var products: [Product] = []
    @Published private(set) var loadingState: LoadingState = .idle

    func start() {
        Task {
            await loadProducts()
            await refreshEntitlements()
            await observeTransactions()
        }
    }
}

The implementation should cache the last known entitlement:

struct CachedEntitlement: Codable {
    let tier: String
    let expirationDate: Date?
    let verifiedAt: Date
}

The app should not become unusable because StoreKit product loading is slow. If the last verified state was Plus and the subscription is likely still valid, the app can show cached access while refreshing. If there is no verified access, the free path should remain usable.

StoreKit failure should not produce a blank app

A common mistake is making the paywall the only route forward while products load.

I model the paywall state explicitly:

enum PaywallState {
    case loading
    case ready(products: [Product])
    case unavailable(message: String, canContinueFree: Bool)
}

The UI can then degrade:

switch state {
case .loading:
    ProgressView("Loading plans")
case .ready(let products):
    ProductList(products: products)
case .unavailable(_, let canContinueFree):
    if canContinueFree {
        ContinueFreeButton()
    } else {
        RetryStoreKitButton()
    }
}

For a small utility, the free path is part of reliability. It lets the app remain useful even when commerce infrastructure is temporarily unavailable.

The AI engine should know the mode

Subscription value should not be enforced only by hiding buttons. The engine should receive an allowed mode:

enum DecisionMode {
    case quick
    case compareOptions
    case reflectWithHistory
}

actor DecisionEngine {
    func answer(
        question: String,
        mode: DecisionMode,
        policy: DecisionUsagePolicy
    ) async throws -> DecisionResult {
        guard policy.allows(mode) else {
            throw DecisionError.requiresPlus
        }

        return try await runPrompt(question: question, mode: mode)
    }
}

This prevents accidental premium leakage when a deep feature is reachable from multiple screens.

Metrics that matter

For subscription AI utilities, I track:

Metric Why it matters
Free core completion rate proves the app is usable before purchase
Paywall load failure rate catches StoreKit or network issues
Restore success rate catches entitlement bugs
Limit-hit conversion shows whether the limit matches value
Subscriber feature usage proves recurring value exists

The key is not maximizing paywall impressions. The key is showing the paywall after the user understands the repeated job.

Postmortem checklist

A subscription in a small AI utility should be technically observable. The code should make the value boundary obvious:

  1. Entitlements are centralized.
  2. Usage policy is explicit.
  3. Free and Plus behavior are testable.
  4. StoreKit failure has a graceful path.
  5. AI modes enforce access rules below the UI layer.

If the only difference between free and paid is a string on a paywall, the product is under-designed. A durable subscription starts as architecture before it becomes pricing.