Contents

App Store Review Guideline 2.1 (Performance - App Completeness): Fix Subscription Page Infinite Loading (Receipt Validation and Frontend Timeout)

App Store Review Guideline 2.1 (Performance - App Completeness): Fix Subscription Page Infinite Loading (Receipt Validation and Frontend Timeout)

A common rejection reason: subscription page spins forever due to improper receipt validation and missing frontend timeouts.


1. Issue Context (Guideline 2.1)

  • Symptom: loading never ends; poor UX.
  • Causes:
    • Backend only calls production endpoint; sandbox receipts not handled (must handle 21007).
    • Frontend requests have no timeout or error UI, leading to infinite waiting.

2. Solution Overview

  1. Backend: production-first receipt validation with sandbox fallback; enforce timeouts.
  2. Frontend: set request timeouts; show loading state; surface errors; allow retry.
  3. Platform: ensure paid apps agreement is accepted in App Store Connect.

3. Backend: Receipt Validation (Python/Requests)

import requests

APPLE_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt"
APPLE_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"

TIMEOUT_SECONDS = 10


def validate_receipt(receipt_data: str, password: str) -> dict:
    """Try production first; if status == 21007 then retry sandbox. Apply request timeouts."""
    payload = {
        "receipt-data": receipt_data,
        "password": password,
        "exclude-old-transactions": True,
    }

    # 1) production first
    resp = requests.post(APPLE_PRODUCTION_URL, json=payload, timeout=TIMEOUT_SECONDS)
    result = resp.json()

    # 2) sandbox fallback
    if result.get("status") == 21007:
        resp = requests.post(APPLE_SANDBOX_URL, json=payload, timeout=TIMEOUT_SECONDS)
        result = resp.json()

    return result

Key points:

  • Handle status == 21007.
  • Timeouts for each request (e.g., 10s) to avoid indefinite waits.
  • Log status and key fields for troubleshooting.

4. Frontend: SwiftUI Request with Timeout + Retry

import SwiftUI

struct SubscriptionView: View {
    @State private var isLoading = false
    @State private var errorMessage: String?

    private let timeout: TimeInterval = 10

    func validateReceipt(receiptData: String, password: String) {
        isLoading = true
        errorMessage = nil
        guard let url = URL(string: "https://your-backend.com/validate-receipt") else {
            errorMessage = "Invalid request URL"
            return
        }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.timeoutInterval = timeout
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let body: [String: Any] = [
            "receipt_data": receiptData,
            "password": password
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                isLoading = false
                if let error = error {
                    errorMessage = "Network error: \(error.localizedDescription)"
                    return
                }
                guard let data = data else {
                    errorMessage = "No data received"
                    return
                }
                // TODO: parse data and update UI as needed
                print("receipt validation response: \(data)")
            }
        }.resume()
    }

    var body: some View {
        VStack(spacing: 12) {
            if isLoading { ProgressView("Validating…") }
            if let errorMessage = errorMessage {
                Text(errorMessage).foregroundColor(.red)
                Button("Retry") {
                    // call validateReceipt again according to your flow
                }
            }
            // Subscription UI …
        }
        .padding()
    }
}

Key points:

  • Use URLRequest.timeoutInterval to avoid indefinite waits; surface errors and offer retry.
  • Visible loading state prevents “silent stuck” screens.

5. App Store Connect Checklist

  • Account holder accepted the Paid Apps Agreement in App Store Connect > Agreements.
  • Test on the same environment/device version as review (e.g., iPadOS 18.5).

6. Self-check

  • Backend handles 21007; timeouts and error logs in place.
  • Frontend timeout ≤ 10–15s; visible error message; retry enabled.
  • Subscription page shows clear loading and failure states; no infinite spinner.
  • Agreements signed; testing passed before resubmission.

Summary

  • Backend: production-first + sandbox fallback + timeout.
  • Frontend: loading, timeout, error handling, and retry.
  • Platform: agreements and device tests improve approval success.