InvestmentTrackerApp/InvestmentTracker/Services/IAPService.swift

226 lines
6.2 KiB
Swift

import Foundation
import StoreKit
import Combine
@MainActor
class IAPService: ObservableObject {
// MARK: - Published Properties
@Published private(set) var isPremium = false
@Published private(set) var products: [Product] = []
@Published private(set) var purchaseState: PurchaseState = .idle
@Published private(set) var isFamilyShared = false
// MARK: - Constants
static let premiumProductID = "com.investmenttracker.premium"
static let premiumPrice = "€4.69"
// MARK: - Private Properties
private var updateListenerTask: Task<Void, Error>?
private var cancellables = Set<AnyCancellable>()
// MARK: - Purchase State
enum PurchaseState: Equatable {
case idle
case purchasing
case purchased
case failed(String)
case restored
}
// MARK: - Initialization
init() {
updateListenerTask = listenForTransactions()
Task {
await loadProducts()
await updatePremiumStatus()
}
}
deinit {
updateListenerTask?.cancel()
}
// MARK: - Load Products
func loadProducts() async {
do {
products = try await Product.products(for: [Self.premiumProductID])
print("Loaded \(products.count) products")
} catch {
print("Failed to load products: \(error)")
}
}
// MARK: - Purchase
func purchase() async throws {
guard let product = products.first else {
throw IAPError.productNotFound
}
purchaseState = .purchasing
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
// Check if family shared
isFamilyShared = transaction.ownershipType == .familyShared
await transaction.finish()
await updatePremiumStatus()
purchaseState = .purchased
// Track analytics
FirebaseService.shared.logPurchaseSuccess(
productId: product.id,
price: product.price,
isFamilyShared: isFamilyShared
)
case .userCancelled:
purchaseState = .idle
case .pending:
purchaseState = .idle
@unknown default:
purchaseState = .idle
}
} catch {
purchaseState = .failed(error.localizedDescription)
FirebaseService.shared.logPurchaseFailure(
productId: product.id,
error: error.localizedDescription
)
throw error
}
}
// MARK: - Restore Purchases
func restorePurchases() async {
purchaseState = .purchasing
do {
try await AppStore.sync()
await updatePremiumStatus()
if isPremium {
purchaseState = .restored
} else {
purchaseState = .failed("No purchases to restore")
}
} catch {
purchaseState = .failed(error.localizedDescription)
}
}
// MARK: - Update Premium Status
func updatePremiumStatus() async {
var isEntitled = false
var familyShared = false
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if transaction.productID == Self.premiumProductID {
isEntitled = true
familyShared = transaction.ownershipType == .familyShared
break
}
}
}
isPremium = isEntitled
isFamilyShared = familyShared
// Update Core Data
let context = CoreDataStack.shared.viewContext
PremiumStatus.updateStatus(
isPremium: isEntitled,
productIdentifier: Self.premiumProductID,
transactionId: nil,
isFamilyShared: familyShared,
in: context
)
}
// MARK: - Transaction Listener
private func listenForTransactions() -> Task<Void, Error> {
return Task.detached { [weak self] in
for await result in Transaction.updates {
if case .verified(let transaction) = result {
await transaction.finish()
await self?.updatePremiumStatus()
}
}
}
}
// MARK: - Verification
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw IAPError.verificationFailed(error.localizedDescription)
case .verified(let safe):
return safe
}
}
// MARK: - Product Info
var premiumProduct: Product? {
products.first { $0.id == Self.premiumProductID }
}
var formattedPrice: String {
premiumProduct?.displayPrice ?? Self.premiumPrice
}
}
// MARK: - IAP Error
enum IAPError: LocalizedError {
case productNotFound
case verificationFailed(String)
case purchaseFailed(String)
var errorDescription: String? {
switch self {
case .productNotFound:
return "Product not found. Please try again later."
case .verificationFailed(let message):
return "Verification failed: \(message)"
case .purchaseFailed(let message):
return "Purchase failed: \(message)"
}
}
}
// MARK: - Premium Features
extension IAPService {
static let premiumFeatures: [(icon: String, title: String, description: String)] = [
("infinity", "Unlimited Sources", "Track as many investments as you want"),
("clock.arrow.circlepath", "Full History", "Access your complete investment history"),
("chart.bar.xaxis", "Advanced Charts", "5 types of detailed analytics charts"),
("wand.and.stars", "Predictions", "AI-powered 12-month forecasts"),
("square.and.arrow.up", "Export Data", "Export to CSV and JSON formats"),
("xmark.circle", "No Ads", "Ad-free experience forever"),
("person.2", "Family Sharing", "Share with up to 5 family members")
]
}