import Foundation import StoreKit typealias Transaction = StoreKit.Transaction typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState public enum StoreError: Error { case failedVerification } public enum ServiceEntitlement: Int, Comparable { case notEntitled = 0 case pro1Month = 1 case pro6Month = 2 case pro1Year = 3 case pro1MonthFamily = 4 case pro6MonthFamily = 5 case pro1YearFamily = 6 case pro3Month = 7 case proLifeTime = 8 init?(for product: Product) { // The product must be a subscription to have service entitlements. guard let subscription = product.subscription else { return nil } if #available(iOS 16.4, *) { self.init(rawValue: subscription.groupLevel) } else { switch product.id { // Plans actifs case "monthlyPlan": self = .pro1Month case "semesterlyPlan": self = .pro6Month case "yearlyPlan": self = .pro1Year case "monthlyPlanFamily": self = .pro1MonthFamily case "semesterlyPlanFamily": self = .pro6MonthFamily case "yearlyPlanFamily": self = .pro1YearFamily // Anciens plans qui doivent être reconnus case "lifetimeProPlan": self = .proLifeTime case "quarterlyPlan": self = .pro3Month default: self = .notEntitled } } } public static func < (lhs: Self, rhs: Self) -> Bool { // Subscription-group levels are in descending order. return lhs.rawValue > rhs.rawValue } } class Store: ObservableObject { let productIDProPlan = "lifetimeProPlan" let productIDSNotAvailable = ["quarterlyPlan"] let productIDSSingle = ["monthlyPlan", "semesterlyPlan", "yearlyPlan"] let productIDSFamily = ["monthlyPlanFamily", "semesterlyPlanFamily", "yearlyPlanFamily"] @Published private(set) var singleSubscriptions: [Product] = [] @Published private(set) var familySubscriptions: [Product] = [] @Published private(set) var notAvailableSubscriptions: [Product] = [] @Published private(set) var nonRenewablesSubscriptions: [Product] = [] @Published private(set) var purchasedSubscriptions: [Product] = [] @Published private(set) var purchasedNonRenewableSubscriptions: [Product] = [] @Published private(set) var subscriptionGroupStatus: Product.SubscriptionInfo.Status? var hasProAccount: Bool { return !purchasedSubscriptions.isEmpty || !purchasedNonRenewableSubscriptions.isEmpty } var updateListenerTask: Task? = nil init() { // Start a transaction listener as close to app launch as possible so you don't miss any transactions. updateListenerTask = listenForTransactions() Task { // During store initialization, request products from the App Store. await requestProducts() // Deliver products that the customer purchases. await updateCustomerProductStatus() } } deinit { updateListenerTask?.cancel() } @MainActor func requestProducts() async { do { // Request single products let storeSingleProducts = try await Product.products(for: productIDSSingle) let storeFamilyProducts = try await Product.products(for: productIDSFamily) let storeNotAvailableProducts = try await Product.products(for: productIDSNotAvailable) let storeNonRenewablesProducts = try await Product.products(for: [productIDProPlan]) // Filter and sort single products singleSubscriptions = sortByPrice(storeSingleProducts) // Filter and sort family products familySubscriptions = sortByPrice(storeFamilyProducts) notAvailableSubscriptions = sortByPrice(storeNotAvailableProducts) nonRenewablesSubscriptions = sortByPrice(storeNonRenewablesProducts) } catch { print("Failed product request from the App Store server. \(error)") } } func purchase(_ product: Product) async throws -> Result { // Begin purchasing the Product the user selected. do { #if os(macOS) let result = try await product.purchase() #else let result: Product.PurchaseResult if #available(iOS 18.2, tvOS 18.2, *), let currentViewController = await UIApplication.shared.currentViewController { result = try await product.purchase(confirmIn: currentViewController) } else { result = try await product.purchase() } #endif switch result { case .success(let verification): // Check whether the transaction is verified. let transaction = try checkVerified(verification) // The transaction is verified. Deliver content to the user. await updateCustomerProductStatus() // Always finish a transaction. await transaction.finish() return .success(transaction) case .userCancelled: return .failure(.userCancelled) case .pending: return .failure(.pending) default: return .failure(.purchaseUnknowError) } } catch { print(error) return .failure(.purchaseUnknowError) } } /* // OLD func purchase(_ product: Product) async throws -> Result { // Begin purchasing the `Product` the user selected. let result = try await product.purchase() switch result { case .success(let verification): // Check whether the transaction is verified. let transaction = try checkVerified(verification) // The transaction is verified. Deliver content to the user. await updateCustomerProductStatus() // Always finish a transaction. await transaction.finish() return .success(transaction) case .userCancelled: return .failure(.userCancelled) case .pending: return .failure(.pending) default: return .failure(.purchaseUnknowError) } } */ func sortByPrice(_ products: [Product]) -> [Product] { products.sorted(by: { return $0.price < $1.price }) } func listenForTransactions() -> Task { return Task.detached { // Iterate through any transactions that don't come from a direct call to `purchase()`. for await result in Transaction.updates { do { let transaction = try self.checkVerified(result) // Deliver products to the user. await self.updateCustomerProductStatus() // Always finish a transaction. await transaction.finish() } catch { // StoreKit has a transaction that fails verification. Don't deliver content to the user. print("Transaction failed verification.") } } } } func checkVerified(_ result: VerificationResult) throws -> T { // Check whether the JWS passes StoreKit verification. switch result { case .unverified: // StoreKit parses the JWS, but it fails verification. throw StoreError.failedVerification case .verified(let safe): // The result is verified. Return the unwrapped value. return safe } } @MainActor func updateCustomerProductStatus() async { var purchasedSubscriptions: [Product] = [] var purchasedNonRenewableSubscriptions: [Product] = [] // Iterate through all of the user's purchased products. for await result in Transaction.currentEntitlements { do { // Check whether the transaction is verified. If it isn't, catch `failedVerification` error. let transaction = try checkVerified(result) // Check the `productType` of the transaction and get the corresponding product from the store. switch transaction.productType { case .nonConsumable: if let nonRenewable = nonRenewablesSubscriptions.first(where: { $0.id == transaction.productID }), transaction.productID == productIDProPlan { purchasedNonRenewableSubscriptions.append(nonRenewable) } case .autoRenewable: if let subscription = singleSubscriptions.first(where: { $0.id == transaction.productID }) { purchasedSubscriptions.append(subscription) } else if let subscription = familySubscriptions.first(where: { $0.id == transaction.productID }) { purchasedSubscriptions.append(subscription) } else if let subscription = notAvailableSubscriptions.first(where: { $0.id == transaction.productID }) { purchasedSubscriptions.append(subscription) } default: break } } catch { print("Transaction failed verification") } } // Update the store information with auto-renewable subscription products. self.purchasedSubscriptions = purchasedSubscriptions self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions // Vérifier le statut des abonnements let allSubscriptions = singleSubscriptions + familySubscriptions + nonRenewablesSubscriptions + notAvailableSubscriptions subscriptionGroupStatus = try? await allSubscriptions.first?.subscription?.status.max { lhs, rhs in // There may be multiple statuses for different family members, because this app supports Family Sharing. // The subscriber is entitled to service for the status with the highest level of service. let lhsEntitlement = entitlement(for: lhs) let rhsEntitlement = entitlement(for: rhs) return lhsEntitlement < rhsEntitlement } } // Get a subscription's level of service using the product ID. func entitlement(for status: Product.SubscriptionInfo.Status) -> ServiceEntitlement { // If the status is expired, then the customer is not entitled. if status.state == .expired || status.state == .revoked { return .notEntitled } // Get the product associated with the subscription status. let productID = status.transaction.unsafePayloadValue.productID // Check in both single and family subscriptions if let product = singleSubscriptions.first(where: { $0.id == productID }) { return ServiceEntitlement(for: product) ?? .notEntitled } else if let product = familySubscriptions.first(where: { $0.id == productID }) { return ServiceEntitlement(for: product) ?? .notEntitled } return .notEntitled } func isPurchased(_ product: Product) -> Bool { // Determine whether the user purchases a given product. switch product.type { case .nonConsumable: return purchasedNonRenewableSubscriptions.contains(product) case .autoRenewable: return purchasedSubscriptions.contains(product) default: return false } } } extension Store { func restorePurchases() async throws { // Synchronise avec l'App Store pour restaurer les achats try await AppStore.sync() // Met à jour le statut des produits après la restauration await updateCustomerProductStatus() } // Cette fonction n'est plus nécessaire car nous utilisons directement AppStore.presentCodeRedemptionSheet() // dans ProAccountController, mais je la laisse ici comme référence #if os(iOS) func redeemPromoCode() async throws { guard let scene = await UIApplication.shared.connectedScenes.first as? UIWindowScene else { throw PurchaseError.sceneNotFound } try await AppStore.presentOfferCodeRedeemSheet(in: scene) } #endif // Fonction utilitaire pour obtenir les informations de prix avec gestion des offres promotionnelles func getPriceInformation(for product: Product) -> (displayPrice: String, originalPrice: String?) { let displayPrice = product.displayPrice // Vérifie s'il y a une offre promotionnelle if let intro = product.subscription?.introductoryOffer { return (displayPrice: intro.price.formatted(), originalPrice: displayPrice) } return (displayPrice: displayPrice, originalPrice: nil) } } enum PurchaseError: Error { case productNotFound case invalidPromoCode case sceneNotFound case userCancelled case pending case purchaseUnknowError } extension Store { enum productIdentifiers:String, CaseIterable { case pro1Month = "monthlyPlan" case pro6Month = "semesterlyPlan" case pro1Year = "yearlyPlan" case pro1MonthFamily = "monthlyPlanFamily" case pro6MonthFamily = "semesterlyPlanFamily" case pro1YearFamily = "yearlyPlanFamily" case pro3Month = "quarterlyPlan" case proLifeTime = "lifetimeProPlan" } func checkPurchase(for productIdentifier: String) async -> Bool { guard let verificationResult = await Transaction.latest(for: productIdentifier) else { return false } switch verificationResult { case .verified(let transaction): // Vérifions si l'abonnement est toujours actif if transaction.productType == .autoRenewable { let currentDate = Date() return transaction.expirationDate?.compare(currentDate) == .orderedDescending } // Pour les achats non renouvelables (comme votre lifetimeProPlan) return true case .unverified: return false } } func hasPurchases() async -> Bool { return true var hasPurchases = false for productId in productIdentifiers.allCases { let isPurchased = await checkPurchase(for: productId.rawValue) if isPurchased { hasPurchases = true } } return hasPurchases } }