diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index b6ea365..f33ca16 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -59,6 +59,8 @@ class DataStore: ObservableObject { store.synchronizationApiURL = "https://xlr.alwaysdata.net/api/" var synchronized : Bool = true + _ = Guard.main // init + #if DEBUG if let server = PListReader.readString(plist: "local", key: "server") { store.synchronizationApiURL = server @@ -105,13 +107,19 @@ class DataStore: ObservableObject { } @objc func collectionDidLoad(notification: Notification) { - self.objectWillChange.send() + + DispatchQueue.main.async { + self.objectWillChange.send() + } + if let userSingleton: StoredSingleton = notification.object as? StoredSingleton { self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? User.placeHolder() } else if let clubsCollection: StoredCollection = notification.object as? StoredCollection { self._fixMissingClubCreatorIfNecessary(clubsCollection) } else if let eventsCollection: StoredCollection = notification.object as? StoredCollection { self._fixMissingEventCreatorIfNecessary(eventsCollection) + } else if let purchaseCollection: StoredCollection = notification.object as? StoredCollection { + try? purchaseCollection.deleteAll() } } diff --git a/PadelClub/Data/README.md b/PadelClub/Data/README.md index 1f51cd5..88bb6de 100644 --- a/PadelClub/Data/README.md +++ b/PadelClub/Data/README.md @@ -6,6 +6,7 @@ Dans Swift: - Ajouter la codingKey correspondante - Pour la classe **Tournament**, ajouter le champ dans l'encoding/decoding - Ouvrir **ServerDataTests** et ajouter un test sur le champ + - Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple Dans Django: - Ajouter le champ dans la classe diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index ec62254..519b263 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -10,9 +10,12 @@ import CoreLocation import LeStorage struct UmpireView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @EnvironmentObject var dataStore: DataStore + @State private var presentSearchView: Bool = false + @State private var showSubscriptions: Bool = false var lastDataSource: String? { dataStore.appSettings.lastDataSource @@ -35,13 +38,22 @@ struct UmpireView: View { PurchaseListView() Section { - NavigationLink { - SubscriptionView() + Button { + self.showSubscriptions = true } label: { Label("Les offres", systemImage: "bookmark.fill") } } + +// Section { +// NavigationLink { +// SubscriptionView() +// } label: { +// Label("Les offres", systemImage: "bookmark.fill") +// } +// } + NavigationLink { MainUserView() } label: { @@ -153,6 +165,11 @@ struct UmpireView: View { } #endif } + .sheet(isPresented: self.$showSubscriptions, content: { + NavigationStack { + SubscriptionView() + } + }) .sheet(isPresented: $presentSearchView) { let user = dataStore.user NavigationStack { diff --git a/PadelClub/Views/Subscription/Guard.swift b/PadelClub/Views/Subscription/Guard.swift index c340a3b..a220a09 100644 --- a/PadelClub/Views/Subscription/Guard.swift +++ b/PadelClub/Views/Subscription/Guard.swift @@ -86,30 +86,51 @@ import LeStorage @MainActor func updatePurchasedIdentifiers(_ transaction: StoreKit.Transaction) async { - if transaction.revocationDate == nil { - // If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`. - purchasedTransactions.insert(transaction) - - do { + + do { + if transaction.revocationDate == nil { + // If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`. + purchasedTransactions.insert(transaction) + + try self._addPurchaseIfPossible(transaction: transaction) + +// if self._purchaseById(transaction.originalID) == nil { +// let purchase: Purchase = transaction.purchase() +// try self.purchases.addOrUpdate(instance: purchase) +// } + } else { + // If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`. + purchasedTransactions.remove(transaction) + try self._updatePurchaseIfPossible(transaction: transaction) + } + + } catch { + Logger.error(error) + } + + self._updateBestPlan() + } + + fileprivate func _addPurchaseIfPossible(transaction: StoreKit.Transaction) throws { + if self.purchases.hasLoadedFromServer { + if self._purchaseById(transaction.originalID) == nil { let purchase: Purchase = transaction.purchase() -// let json = try? purchase.jsonString() ?? "nope" -// Logger.log("Add or update purchase = \(json) ") try self.purchases.addOrUpdate(instance: purchase) - } catch { - Logger.error(error) } - } else { - // If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`. - purchasedTransactions.remove(transaction) - if let existing = self.purchases.first(where: { $0.identifier == transaction.originalID }) { - do { - try self.purchases.delete(instance: existing) - } catch { - Logger.error(error) - } + } + } + + fileprivate func _updatePurchaseIfPossible(transaction: StoreKit.Transaction) throws { + if self.purchases.hasLoadedFromServer { + if let existing: Purchase = self._purchaseById(transaction.originalID) { + existing.revocationDate = transaction.revocationDate + try self.purchases.addOrUpdate(instance: existing) } } - self._updateBestPlan() + } + + fileprivate func _purchaseById(_ transactionId: UInt64) -> Purchase? { + return self.purchases.first(where: { $0.identifier == transactionId }) } func processTransactionResult(_ result: VerificationResult) async throws -> StoreKit.Transaction { @@ -123,19 +144,19 @@ import LeStorage } var currentPlan: StoreItem? { - #if DEBUG - return nil - #else +// #if DEBUG +// return nil +// #else if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) { return plan } return nil - #endif +// #endif } func userFilteredPurchases() -> [StoreKit.Transaction] { Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)") - guard let currentUserUUID = Store.main.currentUserUUID else { + guard let currentUserUUID: UUID = Store.main.currentUserUUID else { return [] } @@ -179,18 +200,18 @@ import LeStorage return Tournament.TournamentPayment.unlimited case .fivePerMonth: if let purchaseDate = self.currentBestPlan?.originalPurchaseDate { - let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.isCanceled == false } + let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false } if tournaments.count < StoreItem.five { return Tournament.TournamentPayment.subscriptionUnit } } return nil default: - let freelyPayed = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count + let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count if freelyPayed < 1 { return Tournament.TournamentPayment.free } - let tournamentCreditCount = self._purchasedTournamentCount() + let tournamentCreditCount: Int = self._purchasedTournamentCount() let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == .unit && $0.isCanceled == false }.count if tournamentCreditCount > unitlyPayed { return Tournament.TournamentPayment.unit diff --git a/PadelClub/Views/Subscription/Purchase.swift b/PadelClub/Views/Subscription/Purchase.swift index 6954488..30acf7b 100644 --- a/PadelClub/Views/Subscription/Purchase.swift +++ b/PadelClub/Views/Subscription/Purchase.swift @@ -20,8 +20,9 @@ class Purchase: ModelObject, Storable { var purchaseDate: Date var productId: String var quantity: Int? + var revocationDate: Date? = nil - init(user: String, identifier: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil) { + init(user: String, identifier: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil, revocationDate: Date? = nil) { self.user = user self.identifier = identifier self.purchaseDate = purchaseDate diff --git a/PadelClub/Views/Subscription/StoreItem.swift b/PadelClub/Views/Subscription/StoreItem.swift index 8f1a7c1..b0fc3d7 100644 --- a/PadelClub/Views/Subscription/StoreItem.swift +++ b/PadelClub/Views/Subscription/StoreItem.swift @@ -12,7 +12,7 @@ enum StoreItem: String, Identifiable, CaseIterable { case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month" case unit = "app.padelclub.tournament.unit" - static let five: Int = 5 + static let five: Int = 2 var id: String { return self.rawValue } diff --git a/PadelClub/Views/Subscription/StoreManager.swift b/PadelClub/Views/Subscription/StoreManager.swift index 1ae252c..c8d4921 100644 --- a/PadelClub/Views/Subscription/StoreManager.swift +++ b/PadelClub/Views/Subscription/StoreManager.swift @@ -49,10 +49,9 @@ class StoreManager { @MainActor func requestProducts() async { do { - let identifiers: [String] = StoreItem.allCases.map { $0.rawValue } - Logger.log("Request products: \(identifiers)") +// let identifiers: [String] = StoreItem.allCases.map { $0.rawValue } - var products: [Product] = try await Product.products(for: identifiers) + var products: [Product] = try await Product.products(for: self._productIdentifiers()) products = products.sorted { p1, p2 in return p2.price > p1.price } @@ -65,6 +64,19 @@ class StoreManager { } } + fileprivate func _productIdentifiers() -> [String] { + var items: [StoreItem] = [] + switch Guard.main.currentPlan { + case .fivePerMonth: + items = [StoreItem.unit, StoreItem.monthlyUnlimited] + case .monthlyUnlimited: + break + default: + items = StoreItem.allCases + } + return items.map { $0.rawValue } + } + func listenForTransactions() -> Task { return Task.detached { //Iterate through any transactions which didn't come from a direct call to `purchase()`. @@ -106,7 +118,7 @@ class StoreManager { case .success(let verificationResult): let transaction = try await Guard.main.processTransactionResult(verificationResult) - + // Always finish a transaction. await transaction.finish() diff --git a/PadelClub/Views/Subscription/SubscriptionView.swift b/PadelClub/Views/Subscription/SubscriptionView.swift index 657bb49..a4481db 100644 --- a/PadelClub/Views/Subscription/SubscriptionView.swift +++ b/PadelClub/Views/Subscription/SubscriptionView.swift @@ -38,6 +38,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate { @Published var error: Error? = nil @Published var isLoading: Bool = false + @Published var isPurchasing: Bool = false @Published var selectedProduct: Product? = nil { didSet { self._computePrice() @@ -61,7 +62,6 @@ class SubscriptionModel: ObservableObject, StoreDelegate { } func productsReceived(products: [Product]) { - self.isLoading = false self.products = products Logger.log("products received = \(products.count)") @@ -78,17 +78,22 @@ class SubscriptionModel: ObservableObject, StoreDelegate { Logger.w("missing product or store manager") return } + self.isPurchasing = true + Task { do { if product.item.isConsumable { if let _ = try await storeManager.purchase(product, quantity: self.quantity) { + self.isPurchasing = false self.showSuccessfulPurchaseView = true } } else { let _ = try await storeManager.purchase(product) + self.isPurchasing = false } } catch { Logger.error(error) + self.isPurchasing = false } } } @@ -173,11 +178,19 @@ struct SubscriptionView: View { } } label: { HStack { - Text("Acheter") - Spacer() - Text(self.model.totalPrice) - }.padding(8.0) - .fontWeight(.bold) + if self.model.isPurchasing { + Spacer() + ProgressView().tint(.white) + Spacer() + } else { + Text("Acheter") + Spacer() + Text(self.model.totalPrice) + } + } + .padding(8.0) + .fontWeight(.bold) + } .buttonStyle(.borderedProminent) .tint(.orange) diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 3fa68ca..31235a2 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -334,4 +334,24 @@ final class ServerDataTests: XCTestCase { } + func testPurchase() async throws { + + guard let userId = Store.main.currentUserUUID?.uuidString.lowercased() else { + assertionFailure("missing user UUID") + return + } + + let purchase: Purchase = Purchase(user: userId, identifier: 1234, purchaseDate: Date(), productId: "productId", quantity: 3, revocationDate: Date()) + let p: Purchase = try await Store.main.service().post(purchase) + + assert(p.id == purchase.id) + assert(p.identifier == purchase.identifier) + assert(p.productId == purchase.productId) + assert(p.purchaseDate.formatted() == purchase.purchaseDate.formatted()) + assert(p.quantity == purchase.quantity) + assert(p.user == purchase.user) + assert(p.revocationDate?.formatted() == purchase.revocationDate?.formatted()) + + } + }