Эх сурвалжийг харах

Improve StoreKit purchase reliability and restore UX.

Add resilient product loading with retry/ordering and provide clear restore progress and outcome messaging so users and App Review can complete purchase validation reliably.

Made-with: Cursor
Hussain Afzal 3 сар өмнө
parent
commit
69d9020778

+ 22 - 10
google_apps/PremiumFeaturesView.swift

@@ -86,18 +86,24 @@ struct PremiumFeaturesView: View {
                         Button {
                             Task { await premiumStore.restorePurchases() }
                         } label: {
-                            Text("Restore")
-                                .font(.system(size: 11, weight: .semibold))
-                                .foregroundStyle(primaryTextColor)
-                                .padding(.horizontal, 8)
-                                .padding(.vertical, 3)
-                                .background(
-                                    RoundedRectangle(cornerRadius: 7, style: .continuous)
-                                        .fill(chipFillColor)
-                                )
+                            HStack(spacing: 6) {
+                                if premiumStore.restoreInProgress {
+                                    ProgressView()
+                                        .controlSize(.mini)
+                                }
+                                Text(premiumStore.restoreInProgress ? "Restoring..." : "Restore")
+                                    .font(.system(size: 11, weight: .semibold))
+                            }
+                            .foregroundStyle(primaryTextColor)
+                            .padding(.horizontal, 8)
+                            .padding(.vertical, 3)
+                            .background(
+                                RoundedRectangle(cornerRadius: 7, style: .continuous)
+                                    .fill(chipFillColor)
+                            )
                         }
                         .buttonStyle(.plain)
-                        .disabled(premiumStore.purchaseInProgress)
+                        .disabled(premiumStore.purchaseInProgress || premiumStore.restoreInProgress)
                     }
 
                     Text("Upgrade to unlock all Premium Features")
@@ -117,6 +123,12 @@ struct PremiumFeaturesView: View {
                             .foregroundStyle(.orange.opacity(0.95))
                     }
 
+                    if let restoreMessage = premiumStore.restoreMessage {
+                        Text(restoreMessage)
+                            .font(.system(size: 11, weight: .medium))
+                            .foregroundStyle(.green.opacity(0.95))
+                    }
+
                     LazyVGrid(
                         columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)],
                         spacing: 10

+ 26 - 1
google_apps/PremiumStore.swift

@@ -18,7 +18,9 @@ final class PremiumStore: ObservableObject {
     @Published private(set) var isLoadingProducts = false
     @Published private(set) var loadError: String?
     @Published private(set) var purchaseInProgress = false
+    @Published private(set) var restoreInProgress = false
     @Published var purchaseError: String?
+    @Published var restoreMessage: String?
 
     private var transactionListener: Task<Void, Never>?
 
@@ -40,6 +42,10 @@ final class PremiumStore: ObservableObject {
     }
 
     func loadProducts() async {
+        await loadProducts(attempt: 1)
+    }
+
+    private func loadProducts(attempt: Int) async {
         isLoadingProducts = true
         loadError = nil
         defer { isLoadingProducts = false }
@@ -47,7 +53,8 @@ final class PremiumStore: ObservableObject {
         do {
             let ids = PremiumProductID.allCases.map(\.rawValue)
             let loaded = try await Product.products(for: ids)
-            products = loaded.sorted { $0.id < $1.id }
+            let order = Dictionary(uniqueKeysWithValues: ids.enumerated().map { ($1, $0) })
+            products = loaded.sorted { (order[$0.id] ?? Int.max) < (order[$1.id] ?? Int.max) }
             if loaded.isEmpty {
                 loadError = """
                 No products returned for IDs: \(ids.joined(separator: ", ")).
@@ -60,9 +67,18 @@ final class PremiumStore: ObservableObject {
                     loadError = "Store returned partial products. Missing IDs: \(missing.joined(separator: ", "))"
                 }
             }
+
+            if products.isEmpty && attempt == 1 {
+                try? await Task.sleep(nanoseconds: 800_000_000)
+                await loadProducts(attempt: 2)
+            }
         } catch {
             let ids = PremiumProductID.allCases.map(\.rawValue).joined(separator: ", ")
             loadError = "StoreKit error: \(error.localizedDescription). Requested IDs: \(ids)"
+            if attempt == 1 {
+                try? await Task.sleep(nanoseconds: 800_000_000)
+                await loadProducts(attempt: 2)
+            }
         }
     }
 
@@ -103,12 +119,21 @@ final class PremiumStore: ObservableObject {
     }
 
     func restorePurchases() async {
+        restoreInProgress = true
+        restoreMessage = nil
         loadError = nil
+        defer { restoreInProgress = false }
         do {
             try await AppStore.sync()
             await refreshEntitlements()
+            if isPremiumUnlocked {
+                restoreMessage = "Purchases restored successfully."
+            } else {
+                restoreMessage = "No active purchases were found for this Apple ID."
+            }
         } catch {
             loadError = error.localizedDescription
+            restoreMessage = nil
         }
     }