App Store Review Guideline 2.1 (Performance - App Completeness): Fix Subscription Page Infinite Loading (Receipt Validation and Frontend Timeout)
Contents
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
- Backend: production-first receipt validation with sandbox fallback; enforce timeouts.
- Frontend: set request timeouts; show loading state; surface errors; allow retry.
- 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.