Applies verified fixes from deep audit (concurrency, performance, security, accessibility), standardizes CRUD form buttons to Add/Save pattern, removes .drawingGroup() that broke search bar TextFields, and converts vulnerable .sheet(isPresented:) + if-let patterns to safe presentation to prevent blank white modals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
2.0 KiB
Swift
64 lines
2.0 KiB
Swift
import Foundation
|
|
import StoreKit
|
|
|
|
/// Shared purchase/restore logic used by FeatureComparisonView, UpgradeFeatureView, and UpgradePromptView.
|
|
/// Each view owns an instance via @StateObject and binds its published properties to the UI.
|
|
@MainActor
|
|
final class SubscriptionPurchaseHelper: ObservableObject {
|
|
@Published var isProcessing = false
|
|
@Published var errorMessage: String?
|
|
@Published var showSuccessAlert = false
|
|
@Published var selectedProduct: Product?
|
|
|
|
private var storeKit: StoreKitManager { StoreKitManager.shared }
|
|
|
|
func handlePurchase(_ product: Product) {
|
|
selectedProduct = product
|
|
isProcessing = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let transaction = try await storeKit.purchase(product)
|
|
|
|
await MainActor.run {
|
|
isProcessing = false
|
|
|
|
if transaction != nil {
|
|
// Check if backend verification failed (purchase is valid but pending server confirmation)
|
|
if let backendError = storeKit.purchaseError {
|
|
errorMessage = backendError
|
|
} else {
|
|
showSuccessAlert = true
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isProcessing = false
|
|
errorMessage = "Purchase failed: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleRestore() {
|
|
isProcessing = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
await storeKit.restorePurchases()
|
|
|
|
await MainActor.run {
|
|
isProcessing = false
|
|
|
|
if !storeKit.purchasedProductIDs.isEmpty {
|
|
showSuccessAlert = true
|
|
} else {
|
|
errorMessage = "No purchases found to restore"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|