From d3c443d1169d0df610bf4b6edd26709b615ebd79 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 4 Sep 2025 11:52:33 +0200 Subject: [PATCH 01/11] adds the pack of 10 units --- PadelClubData/Subscriptions/Guard.swift | 4 +++- PadelClubData/Subscriptions/StoreItem.swift | 4 +++- PadelClubData/Subscriptions/StoreManager.swift | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index aa77c06..f9951ba 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -235,7 +235,9 @@ import Combine let purchases = DataStore.shared.purchases let units = purchases.filter { $0.productId == StoreItem.unit.rawValue } - return units.reduce(0) { $0 + ($1.quantity ?? 0) } + let units10Pack = purchases.filter { $0.productId == StoreItem.unit10Pack.rawValue } + + return units.reduce(0) { $0 + ($1.quantity ?? 0) } + 10 * units10Pack.reduce(0) { $0 + ($1.quantity ?? 0) } // let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue } // return units.reduce(0) { $0 + $1.purchasedQuantity } diff --git a/PadelClubData/Subscriptions/StoreItem.swift b/PadelClubData/Subscriptions/StoreItem.swift index 868d28c..11f2b9f 100644 --- a/PadelClubData/Subscriptions/StoreItem.swift +++ b/PadelClubData/Subscriptions/StoreItem.swift @@ -11,6 +11,7 @@ public enum StoreItem: String, Identifiable, CaseIterable { case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited" case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month" case unit = "app.padelclub.tournament.unit" + case unit10Pack = "app.padelclub.tournament.unit.10" #if DEBUG public static let five: Int = 2 @@ -25,13 +26,14 @@ public enum StoreItem: String, Identifiable, CaseIterable { case .monthlyUnlimited: return "infinity.circle.fill" case .fivePerMonth: return "star.circle.fill" case .unit: return "tennisball.circle.fill" + case .unit10Pack: return "10.circle.fill" } } public var isConsumable: Bool { switch self { case .monthlyUnlimited, .fivePerMonth: return false - case .unit: return true + case .unit, .unit10Pack: return true } } diff --git a/PadelClubData/Subscriptions/StoreManager.swift b/PadelClubData/Subscriptions/StoreManager.swift index 4bf853b..d26f0ef 100644 --- a/PadelClubData/Subscriptions/StoreManager.swift +++ b/PadelClubData/Subscriptions/StoreManager.swift @@ -68,11 +68,11 @@ public class StoreManager { var items: [StoreItem] = [] switch Guard.main.currentPlan { case .fivePerMonth: - items = [StoreItem.unit, StoreItem.monthlyUnlimited] + items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] case .monthlyUnlimited: break default: - items = StoreItem.allCases + items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] } return items.map { $0.rawValue } } From 59642d1aca57b992b997d1cf3a4416e1b2f58969 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 15 Sep 2025 10:51:02 +0200 Subject: [PATCH 02/11] fix a smart planner issue --- PadelClubData/Data/MatchScheduler.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index 5f16a7d..39ed5de 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -838,8 +838,10 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { } - if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { - bracketStartDate = lastDate.tomorrowAtNine + if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || lastDate.timeOfDay == .night || errorFormat) { + if tournament.groupStageCount > 0 { + bracketStartDate = lastDate.tomorrowAtNine + } } return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate) From 9ed8e56c2ec0520d12347dbb09d50f3b4aebddda Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 17 Sep 2025 11:12:56 +0200 Subject: [PATCH 03/11] fix issue with field setup add a way to set price on all tournaments of a month fix the tournament creation not using the most used price fix issue in team detail view regarding registratoin date modification --- PadelClubData/Data/Tournament.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 4d328c2..9c16560 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -1682,12 +1682,14 @@ defer { } public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) { + courtCount = eventObject()?.clubObject()?.courtCount ?? 2 setupDefaultPrivateSettings(templateTournament: templateTournament) setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings if let templateTournament { setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount) } setupFederalSettings() + customizeUsingPreferences() } public func setupFederalSettings() { @@ -1703,6 +1705,23 @@ defer { } } + public func customizeUsingPreferences() { + guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in + tournament.tournamentLevel == self.tournamentLevel + && tournament.tournamentCategory == self.tournamentCategory + && tournament.federalTournamentAge == self.federalTournamentAge + && tournament.hasEnded() == true + && tournament.isCanceled == false + && tournament.isDeleted == false + }).sorted(by: \.endDate!, order: .descending).first else { + return + } + + self.entryFee = lastTournamentWithSameBuild.entryFee + self.clubMemberFeeDeduction = lastTournamentWithSameBuild.clubMemberFeeDeduction + } + + public func deadline(for type: TournamentDeadlineType) -> Date? { guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } From e3b3c6456cbf7722d6e9a714229f1acf46676183 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 17 Sep 2025 15:39:18 +0200 Subject: [PATCH 04/11] minor improvements to Guard --- PadelClubData/Subscriptions/Guard.swift | 45 +++++++++++++++++++------ 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index aa77c06..2ff17e7 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -30,11 +30,7 @@ import Combine self.updateListenerTask = self.listenForTransactions() Task { - do { - try await self.refreshPurchasedAppleProducts() - } catch { - Logger.error(error) - } + await self.refreshPurchases() Logger.log("plan = \(String(describing: currentBestPurchase?.productId))") } @@ -66,19 +62,46 @@ import Combine return productIds } - public func refreshPurchasedAppleProducts() async throws { + public func refreshPurchases() async { + await _refreshUnfinishedTransactions() + await _refreshPurchasedAppleProducts() + } + + fileprivate func _refreshPurchasedAppleProducts() async { // Iterate through the user's purchased products. for await verificationResult in Transaction.currentEntitlements { - let transaction = try await self.processTransactionResult(verificationResult) - print("processs product id = \(transaction.productID)") - DispatchQueue.main.async { - NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil) + do { + let transaction = try await self.processTransactionResult(verificationResult) + print("processs product id = \(transaction.productID)") + DispatchQueue.main.async { + NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil) + } + await transaction.finish() + } catch { + Logger.error(error) } - await transaction.finish() } } + public func _refreshUnfinishedTransactions() async { + + // Iterate through the user's purchased products. + for await verificationResult in Transaction.unfinished { + do { + let transaction = try await self.processTransactionResult(verificationResult) + print("processs product id = \(transaction.productID)") + DispatchQueue.main.async { + NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil) + } + await transaction.finish() + } catch { + Logger.error(error) + } + } + } + + func listenForTransactions() -> Task { return Task(priority: .background) { //Iterate through any transactions which didn't come from a direct call to `purchase()`. From 9bb39dcf8a6e5073abc1d8be42a9cdc55822cce4 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 17 Sep 2025 15:57:44 +0200 Subject: [PATCH 05/11] adds pack of 10 tournaments IAP --- PadelClubData/Subscriptions/StoreItem.swift | 10 +- .../Subscriptions/StoreManager.swift | 6 +- PadelClubDataTests/PadelClubDataTests.swift | 94 ----- PadelClubDataTests/SyncDataAccessTests.swift | 327 ------------------ 4 files changed, 14 insertions(+), 423 deletions(-) delete mode 100644 PadelClubDataTests/PadelClubDataTests.swift delete mode 100644 PadelClubDataTests/SyncDataAccessTests.swift diff --git a/PadelClubData/Subscriptions/StoreItem.swift b/PadelClubData/Subscriptions/StoreItem.swift index 11f2b9f..d9c4bce 100644 --- a/PadelClubData/Subscriptions/StoreItem.swift +++ b/PadelClubData/Subscriptions/StoreItem.swift @@ -21,11 +21,19 @@ public enum StoreItem: String, Identifiable, CaseIterable { public var id: String { return self.rawValue } + public var summarySystemImage: String { + switch self { + case .monthlyUnlimited: return "infinity.circle.fill" + case .fivePerMonth: return "star.circle.fill" + case .unit, .unit10Pack: return "tennisball.circle.fill" + } + } + public var systemImage: String { switch self { case .monthlyUnlimited: return "infinity.circle.fill" case .fivePerMonth: return "star.circle.fill" - case .unit: return "tennisball.circle.fill" + case .unit: return "1.circle.fill" case .unit10Pack: return "10.circle.fill" } } diff --git a/PadelClubData/Subscriptions/StoreManager.swift b/PadelClubData/Subscriptions/StoreManager.swift index d26f0ef..425de9a 100644 --- a/PadelClubData/Subscriptions/StoreManager.swift +++ b/PadelClubData/Subscriptions/StoreManager.swift @@ -53,7 +53,11 @@ public class StoreManager { var products: [Product] = try await Product.products(for: self._productIdentifiers()) products = products.sorted { p1, p2 in - return p2.price > p1.price + if p1.type == p2.type { + return p2.price > p1.price + } else { + return p2.type.rawValue < p1.type.rawValue + } } Logger.log("products = \(products.count)") diff --git a/PadelClubDataTests/PadelClubDataTests.swift b/PadelClubDataTests/PadelClubDataTests.swift deleted file mode 100644 index 18acb93..0000000 --- a/PadelClubDataTests/PadelClubDataTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// PadelClubDataTests.swift -// PadelClubDataTests -// -// Created by Laurent Morvillier on 15/04/2025. -// - -import Testing -@testable import PadelClubData -@testable import LeStorage - -enum TestError: Error { - case notAuthenticated - case sameDeviceId - case missingEvent -} - -struct PadelClubDataTests { - - let username: String = "UserDataTests" - let password: String = "MyPass1234--" - - init() async throws { - StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "token.json") - try await self.login() - } - - mutating func login() async throws { - let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) - } - - @Test func testAuthentication() { - #expect(StoreCenter.main.isAuthenticated) - } - - @Test func createTournament() async throws { - - guard let userId = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - - // Cleanup - let events = DataStore.shared.events - try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) - - try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.events.count == 0) - - try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.tournaments.count == 0) - - // Create - let event: Event = Event(creator: userId, club: nil, name: "test") - try await DataStore.shared.events.addOrUpdateAsync(instance: event) - - let tournament: Tournament = Tournament.fake() - tournament.event = event.id - try await DataStore.shared.tournaments.addOrUpdateAsync(instance: tournament) - - // Test server content - try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.events.count == 1) - - try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) - #expect(DataStore.shared.tournaments.count == 1) - - } - - @Test func dualStoreCenter() async throws { - - let secondStoreServer = StoreCenter() - secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") - - let _: CustomUser = try await secondStoreServer.service().login(username: self.username, password: self.password) - - #expect(StoreCenter.main.isAuthenticated) - #expect(secondStoreServer.isAuthenticated) - - } - - @Test func testWebsocketSynchronization() async throws { - - let secondStoreServer = StoreCenter() - secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") - secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") - - let events = DataStore.shared.events - try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) - - } - -} diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift deleted file mode 100644 index c605dbc..0000000 --- a/PadelClubDataTests/SyncDataAccessTests.swift +++ /dev/null @@ -1,327 +0,0 @@ -// -// DataAccessSyncTests.swift -// PadelClubDataTests -// -// Created by Laurent Morvillier on 02/05/2025. -// - -import Testing -@testable import PadelClubData -@testable import LeStorage - -struct SyncDataAccessTests { - - let username1: String = "UserDataTests" - let password1: String = "MyPass1234--" - - let username2: String = "seconduser" - let password2: String = "MyPass1234--" - - var secondStoreCenter: StoreCenter - - init() async throws { - FileManager.default.deleteDirectoryInDocuments(directoryName: "storage") - FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2") - - self.secondStoreCenter = StoreCenter(directoryName: "storage-2") - self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) - self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json") - self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json") - try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString) - - self.secondStoreCenter.classProject = "PadelClubData" - - let token2 = try? self.secondStoreCenter.rawTokenShouldNotBeUsed() - if token2 == nil { - try await self.login(storeCenter: self.secondStoreCenter, username: self.username2, password: self.password2) - } - - StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) - StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json") - StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json") - try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString) - StoreCenter.main.classProject = "PadelClubData" - - let token = try? StoreCenter.main.rawTokenShouldNotBeUsed() - if token == nil { - try await self.login(storeCenter: StoreCenter.main, username: self.username1, password: self.password1) - } - } - - mutating func login(storeCenter: StoreCenter, username: String, password: String) async throws { - let _: CustomUser = try await storeCenter.service().login(username: username, password: password) - } - - @Test func testSetup() async throws { - #expect(StoreCenter.main.isAuthenticated) - #expect(self.secondStoreCenter.isAuthenticated) - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - #expect(userId1 != userId2) - } - - /// In this test, the first user: - /// - creates an event and a tournament - /// - shares the tournament with a second user - /// - remove the sharing with the second user - /// We test that the data is properly received and removed upon the sharing actions - @Test func testTournamentSharing() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await tournamentColA.deleteAsync(contentOfs: Array(tournamentColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(tournamentColB.count == 0) - - // Create - let eventA = Event(creator: userId1) - try await eventColA.addOrUpdateAsync(instance: eventA) - let tournamentA = Tournament(event: eventA.id, name: "P100") - try await tournamentColA.addOrUpdateAsync(instance: tournamentA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: [userId2]) - - var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.grants.count == 2) - - #expect(eventColB.count == 1) - #expect(tournamentColB.count == 1) - - // Remove sharing from user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: []) - - dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.revocations.count == 1) - #expect(syncDataB.revocationParents.count == 1) - - #expect(eventColB.count == 0) - #expect(tournamentColB.count == 0) - - let dataAccesses: [DataAccess] = try await StoreCenter.main.service().get() - #expect(dataAccesses.count == 0) - } - - /// In this test, the first user: - /// - creates a club and 2 events in that club - /// - shares both events with a second user - /// - removes the sharing of 1 event - /// Here we want to test that the Club instance remains even if one event is removed from the second user - @Test func testSharedRelationship() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let clubA = Club(creator: userId1, name: "Club A", acronym: "CA") - try await clubColA.addOrUpdateAsync(instance: clubA) - - let event1A = Event(creator: userId1, club: clubA.id, name: "event 1") - let event2A = Event(creator: userId1, club: clubA.id, name: "event 2") - try await eventColA.addOrUpdateAsync(contentOfs: [event1A, event2A]) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: [userId2]) - try await StoreCenter.main.setAuthorizedUsersAsync(for: event2A, users: [userId2]) - - var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - - #expect(syncDataB.grants.count == 2) - - let clubGrants = syncDataB.grants.first { $0.type == Club.self } - let eventGrants = syncDataB.grants.first { $0.type == Event.self } - #expect(clubGrants?.items.count == 1) - #expect(eventGrants?.items.count == 2) - - #expect(eventColB.count == 2) - #expect(clubColB.count == 1) - - // Remove sharing from user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: []) - - dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - #expect(syncDataB.revocations.count == 1) - #expect(syncDataB.revocationParents.count == 1) - - #expect(eventColB.count == 1) - #expect(clubColB.count == 1) // club remains because used in event2A - } - - /// In this test, the first user: - /// - creates one event and 2 clubs - /// - shares the event with a second user - /// - changes the club on the event - /// Here we want to test that the first Club is removed and the second one is received - @Test func testRelationshipChange() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let club1A = Club(creator: userId1, name: "Club 1", acronym: "C1") - try await clubColA.addOrUpdateAsync(instance: club1A) - let club2A = Club(creator: userId1, name: "Club 2", acronym: "C2") - try await clubColA.addOrUpdateAsync(instance: club2A) - - let eventA = Event(creator: userId1, club: club1A.id, name: "event 1") - try await eventColA.addOrUpdateAsync(instance: eventA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - #expect(eventColB.count == 1) - #expect(clubColB.count == 1) - - // Change the club - eventA.club = club2A.id - try await eventColA.addOrUpdateAsync(instance: eventA) - let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) - - #expect(syncDataB.sharedRelationshipSets.count == 1) - #expect(syncDataB.sharedRelationshipRemovals.count == 1) - - #expect(eventColB.first?.club == club2A.id) - } - - /// In this test, the first user: - /// - creates one event - /// - shares the event with a second user - /// The second user: - /// - changes the club - /// Here we want to test that the first Club is removed and the second one is received - @Test func testRelationshipChangesByAgent() async throws { - - guard let userId1 = StoreCenter.main.userId else { - throw TestError.notAuthenticated - } - guard let userId2 = self.secondStoreCenter.userId else { - throw TestError.notAuthenticated - } - - // Setup - let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() - let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() - - if let dataAccessCollection = StoreCenter.main.dataAccessCollection { - try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) - } - - try await eventColA.deleteAsync(contentOfs: Array(eventColA)) - try await clubColA.deleteAsync(contentOfs: Array(clubColA)) - - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - #expect(eventColB.count == 0) - #expect(clubColB.count == 0) - - // Create - let clubA = Club(creator: userId1, name: "Club 1", acronym: "C1") - try await clubColA.addOrUpdateAsync(instance: clubA) - - let eventA = Event(creator: userId1, club: clubA.id, name: "event 1") - try await eventColA.addOrUpdateAsync(instance: eventA) - - // Share with user2 - try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) - let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() - - guard let eventB = eventColB.first else { - throw TestError.missingEvent - } - #expect(eventA.id == eventB.id) - #expect(clubColB.count == 1) - - // Second user changes the club - let club2B = Club(creator: userId2, name: "Club 2", acronym: "C2") - try await clubColB.addOrUpdateAsync(instance: club2B) - eventB.club = club2B.id - try await eventColB.addOrUpdateAsync(instance: eventB) - - let dataA = try await StoreCenter.main.testSynchronizeOnceAsync() - let syncDataA = try SyncData(data: dataA, storeCenter: StoreCenter.main) - -// #expect(syncDataA.sharedRelationshipSets.count == 1) -// #expect(syncDataA.sharedRelationshipRemovals.count == 1) - - #expect(eventA.club == club2B.id) - #expect(clubColB.count == 1) - - } - -} From 35b2c6c2a6b48706c82d4b4670e2470ca90337dd Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 18 Sep 2025 11:40:21 +0200 Subject: [PATCH 06/11] update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6cad952..6403bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +PadelClubDataTests/config.plist + ## User settings xcuserdata/ From f1d21b71ee925814678b1f1c86137fe59b2956ee Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 18 Sep 2025 11:43:32 +0200 Subject: [PATCH 07/11] update viewable products in the store --- PadelClubData/Subscriptions/StoreManager.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PadelClubData/Subscriptions/StoreManager.swift b/PadelClubData/Subscriptions/StoreManager.swift index 425de9a..e34f7e5 100644 --- a/PadelClubData/Subscriptions/StoreManager.swift +++ b/PadelClubData/Subscriptions/StoreManager.swift @@ -71,10 +71,8 @@ public class StoreManager { fileprivate func _productIdentifiers() -> [String] { var items: [StoreItem] = [] switch Guard.main.currentPlan { - case .fivePerMonth: - items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] case .monthlyUnlimited: - break + items = [StoreItem.unit, StoreItem.unit10Pack] default: items = [StoreItem.unit, StoreItem.unit10Pack, StoreItem.monthlyUnlimited] } From 306213e2043ae527cb1e2f668712cab1c1cdfc04 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 19 Sep 2025 16:28:24 +0200 Subject: [PATCH 08/11] restore test file --- PadelClubDataTests/PadelClubDataTests.swift | 94 ++++++ PadelClubDataTests/SyncDataAccessTests.swift | 327 +++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 PadelClubDataTests/PadelClubDataTests.swift create mode 100644 PadelClubDataTests/SyncDataAccessTests.swift diff --git a/PadelClubDataTests/PadelClubDataTests.swift b/PadelClubDataTests/PadelClubDataTests.swift new file mode 100644 index 0000000..18acb93 --- /dev/null +++ b/PadelClubDataTests/PadelClubDataTests.swift @@ -0,0 +1,94 @@ +// +// PadelClubDataTests.swift +// PadelClubDataTests +// +// Created by Laurent Morvillier on 15/04/2025. +// + +import Testing +@testable import PadelClubData +@testable import LeStorage + +enum TestError: Error { + case notAuthenticated + case sameDeviceId + case missingEvent +} + +struct PadelClubDataTests { + + let username: String = "UserDataTests" + let password: String = "MyPass1234--" + + init() async throws { + StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "token.json") + try await self.login() + } + + mutating func login() async throws { + let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) + } + + @Test func testAuthentication() { + #expect(StoreCenter.main.isAuthenticated) + } + + @Test func createTournament() async throws { + + guard let userId = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + + // Cleanup + let events = DataStore.shared.events + try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) + + try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.events.count == 0) + + try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.tournaments.count == 0) + + // Create + let event: Event = Event(creator: userId, club: nil, name: "test") + try await DataStore.shared.events.addOrUpdateAsync(instance: event) + + let tournament: Tournament = Tournament.fake() + tournament.event = event.id + try await DataStore.shared.tournaments.addOrUpdateAsync(instance: tournament) + + // Test server content + try await DataStore.shared.events.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.events.count == 1) + + try await DataStore.shared.tournaments.loadDataFromServerIfAllowed(clear: true) + #expect(DataStore.shared.tournaments.count == 1) + + } + + @Test func dualStoreCenter() async throws { + + let secondStoreServer = StoreCenter() + secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") + + let _: CustomUser = try await secondStoreServer.service().login(username: self.username, password: self.password) + + #expect(StoreCenter.main.isAuthenticated) + #expect(secondStoreServer.isAuthenticated) + + } + + @Test func testWebsocketSynchronization() async throws { + + let secondStoreServer = StoreCenter() + secondStoreServer.configureURLs(secureScheme: false, domain: "127.0.0.1:8000") + secondStoreServer.tokenKeychain = MockKeychainStore(fileName: "token.json") + + let events = DataStore.shared.events + try await DataStore.shared.events.deleteAsync(contentOfs: Array(events)) + + } + +} diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift new file mode 100644 index 0000000..c605dbc --- /dev/null +++ b/PadelClubDataTests/SyncDataAccessTests.swift @@ -0,0 +1,327 @@ +// +// DataAccessSyncTests.swift +// PadelClubDataTests +// +// Created by Laurent Morvillier on 02/05/2025. +// + +import Testing +@testable import PadelClubData +@testable import LeStorage + +struct SyncDataAccessTests { + + let username1: String = "UserDataTests" + let password1: String = "MyPass1234--" + + let username2: String = "seconduser" + let password2: String = "MyPass1234--" + + var secondStoreCenter: StoreCenter + + init() async throws { + FileManager.default.deleteDirectoryInDocuments(directoryName: "storage") + FileManager.default.deleteDirectoryInDocuments(directoryName: "storage-2") + + self.secondStoreCenter = StoreCenter(directoryName: "storage-2") + self.secondStoreCenter.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) + self.secondStoreCenter.tokenKeychain = MockKeychainStore(fileName: "storage-2/token.json") + self.secondStoreCenter.deviceKeychain = MockKeychainStore(fileName: "storage-2/device.json") + try self.secondStoreCenter.deviceKeychain.add(value: UUID().uuidString) + + self.secondStoreCenter.classProject = "PadelClubData" + + let token2 = try? self.secondStoreCenter.rawTokenShouldNotBeUsed() + if token2 == nil { + try await self.login(storeCenter: self.secondStoreCenter, username: self.username2, password: self.password2) + } + + StoreCenter.main.configureURLs(secureScheme: false, domain: "127.0.0.1:8000", webSockets: false, useSynchronization: true) + StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "storage/token.json") + StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "storage/device.json") + try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString) + StoreCenter.main.classProject = "PadelClubData" + + let token = try? StoreCenter.main.rawTokenShouldNotBeUsed() + if token == nil { + try await self.login(storeCenter: StoreCenter.main, username: self.username1, password: self.password1) + } + } + + mutating func login(storeCenter: StoreCenter, username: String, password: String) async throws { + let _: CustomUser = try await storeCenter.service().login(username: username, password: password) + } + + @Test func testSetup() async throws { + #expect(StoreCenter.main.isAuthenticated) + #expect(self.secondStoreCenter.isAuthenticated) + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + #expect(userId1 != userId2) + } + + /// In this test, the first user: + /// - creates an event and a tournament + /// - shares the tournament with a second user + /// - remove the sharing with the second user + /// We test that the data is properly received and removed upon the sharing actions + @Test func testTournamentSharing() async throws { + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + + // Setup + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + if let dataAccessCollection = StoreCenter.main.dataAccessCollection { + try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) + } + + try await eventColA.deleteAsync(contentOfs: Array(eventColA)) + try await tournamentColA.deleteAsync(contentOfs: Array(tournamentColA)) + + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + #expect(eventColB.count == 0) + #expect(tournamentColB.count == 0) + + // Create + let eventA = Event(creator: userId1) + try await eventColA.addOrUpdateAsync(instance: eventA) + let tournamentA = Tournament(event: eventA.id, name: "P100") + try await tournamentColA.addOrUpdateAsync(instance: tournamentA) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: [userId2]) + + var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.grants.count == 2) + + #expect(eventColB.count == 1) + #expect(tournamentColB.count == 1) + + // Remove sharing from user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: tournamentA, users: []) + + dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.revocations.count == 1) + #expect(syncDataB.revocationParents.count == 1) + + #expect(eventColB.count == 0) + #expect(tournamentColB.count == 0) + + let dataAccesses: [DataAccess] = try await StoreCenter.main.service().get() + #expect(dataAccesses.count == 0) + } + + /// In this test, the first user: + /// - creates a club and 2 events in that club + /// - shares both events with a second user + /// - removes the sharing of 1 event + /// Here we want to test that the Club instance remains even if one event is removed from the second user + @Test func testSharedRelationship() async throws { + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + + // Setup + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + if let dataAccessCollection = StoreCenter.main.dataAccessCollection { + try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) + } + + try await eventColA.deleteAsync(contentOfs: Array(eventColA)) + try await clubColA.deleteAsync(contentOfs: Array(clubColA)) + + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + #expect(eventColB.count == 0) + #expect(clubColB.count == 0) + + // Create + let clubA = Club(creator: userId1, name: "Club A", acronym: "CA") + try await clubColA.addOrUpdateAsync(instance: clubA) + + let event1A = Event(creator: userId1, club: clubA.id, name: "event 1") + let event2A = Event(creator: userId1, club: clubA.id, name: "event 2") + try await eventColA.addOrUpdateAsync(contentOfs: [event1A, event2A]) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: [userId2]) + try await StoreCenter.main.setAuthorizedUsersAsync(for: event2A, users: [userId2]) + + var dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + var syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + + #expect(syncDataB.grants.count == 2) + + let clubGrants = syncDataB.grants.first { $0.type == Club.self } + let eventGrants = syncDataB.grants.first { $0.type == Event.self } + #expect(clubGrants?.items.count == 1) + #expect(eventGrants?.items.count == 2) + + #expect(eventColB.count == 2) + #expect(clubColB.count == 1) + + // Remove sharing from user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: event1A, users: []) + + dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + #expect(syncDataB.revocations.count == 1) + #expect(syncDataB.revocationParents.count == 1) + + #expect(eventColB.count == 1) + #expect(clubColB.count == 1) // club remains because used in event2A + } + + /// In this test, the first user: + /// - creates one event and 2 clubs + /// - shares the event with a second user + /// - changes the club on the event + /// Here we want to test that the first Club is removed and the second one is received + @Test func testRelationshipChange() async throws { + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + + // Setup + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + if let dataAccessCollection = StoreCenter.main.dataAccessCollection { + try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) + } + + try await eventColA.deleteAsync(contentOfs: Array(eventColA)) + try await clubColA.deleteAsync(contentOfs: Array(clubColA)) + + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + #expect(eventColB.count == 0) + #expect(clubColB.count == 0) + + // Create + let club1A = Club(creator: userId1, name: "Club 1", acronym: "C1") + try await clubColA.addOrUpdateAsync(instance: club1A) + let club2A = Club(creator: userId1, name: "Club 2", acronym: "C2") + try await clubColA.addOrUpdateAsync(instance: club2A) + + let eventA = Event(creator: userId1, club: club1A.id, name: "event 1") + try await eventColA.addOrUpdateAsync(instance: eventA) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + #expect(eventColB.count == 1) + #expect(clubColB.count == 1) + + // Change the club + eventA.club = club2A.id + try await eventColA.addOrUpdateAsync(instance: eventA) + let dataB = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + let syncDataB = try SyncData(data: dataB, storeCenter: self.secondStoreCenter) + + #expect(syncDataB.sharedRelationshipSets.count == 1) + #expect(syncDataB.sharedRelationshipRemovals.count == 1) + + #expect(eventColB.first?.club == club2A.id) + } + + /// In this test, the first user: + /// - creates one event + /// - shares the event with a second user + /// The second user: + /// - changes the club + /// Here we want to test that the first Club is removed and the second one is received + @Test func testRelationshipChangesByAgent() async throws { + + guard let userId1 = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.secondStoreCenter.userId else { + throw TestError.notAuthenticated + } + + // Setup + let eventColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let clubColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let eventColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + let clubColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + if let dataAccessCollection = StoreCenter.main.dataAccessCollection { + try await dataAccessCollection.deleteAsync(contentOfs: Array(dataAccessCollection)) + } + + try await eventColA.deleteAsync(contentOfs: Array(eventColA)) + try await clubColA.deleteAsync(contentOfs: Array(clubColA)) + + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + #expect(eventColB.count == 0) + #expect(clubColB.count == 0) + + // Create + let clubA = Club(creator: userId1, name: "Club 1", acronym: "C1") + try await clubColA.addOrUpdateAsync(instance: clubA) + + let eventA = Event(creator: userId1, club: clubA.id, name: "event 1") + try await eventColA.addOrUpdateAsync(instance: eventA) + + // Share with user2 + try await StoreCenter.main.setAuthorizedUsersAsync(for: eventA, users: [userId2]) + let _ = try await self.secondStoreCenter.testSynchronizeOnceAsync() + + guard let eventB = eventColB.first else { + throw TestError.missingEvent + } + #expect(eventA.id == eventB.id) + #expect(clubColB.count == 1) + + // Second user changes the club + let club2B = Club(creator: userId2, name: "Club 2", acronym: "C2") + try await clubColB.addOrUpdateAsync(instance: club2B) + eventB.club = club2B.id + try await eventColB.addOrUpdateAsync(instance: eventB) + + let dataA = try await StoreCenter.main.testSynchronizeOnceAsync() + let syncDataA = try SyncData(data: dataA, storeCenter: StoreCenter.main) + +// #expect(syncDataA.sharedRelationshipSets.count == 1) +// #expect(syncDataA.sharedRelationshipRemovals.count == 1) + + #expect(eventA.club == club2B.id) + #expect(clubColB.count == 1) + + } + +} From be94ebfc7f17ca9cb7c58f7e3552f71abecafc73 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 15:27:44 +0200 Subject: [PATCH 09/11] The check for payment now also make an api call --- PadelClubData/Subscriptions/Guard.swift | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/PadelClubData/Subscriptions/Guard.swift b/PadelClubData/Subscriptions/Guard.swift index 2164667..bb9ed87 100644 --- a/PadelClubData/Subscriptions/Guard.swift +++ b/PadelClubData/Subscriptions/Guard.swift @@ -266,7 +266,26 @@ import Combine // return units.reduce(0) { $0 + $1.purchasedQuantity } } - public func paymentForNewTournament() -> TournamentPayment? { + struct CanCreateResponse: Decodable { var canCreate: Bool } + + public func paymentForNewTournament() async -> TournamentPayment? { + + if let payment = self.localPaymentForNewTournament() { + return payment + } else if let services = try? StoreCenter.main.service() { + do { + let response: CanCreateResponse = try await services.run(path: "is_granted_unlimited_access/", method: .get, requiresToken: true) + if response.canCreate { + return .unlimited + } + } catch { + Logger.error(error) + } + } + return nil + } + + public func localPaymentForNewTournament() -> TournamentPayment? { switch self.currentPlan { case .monthlyUnlimited: From d45adf1937e6b3fa7d3c22a40921e0e3f3201e87 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Wed, 24 Sep 2025 20:29:00 +0200 Subject: [PATCH 10/11] fix issue with ios 26 add onboarding view fix bugs --- PadelClubData/Data/AppSettings.swift | 5 +++ PadelClubData/Data/GroupStage.swift | 16 +++++++-- PadelClubData/Data/MatchScheduler.swift | 5 +++ PadelClubData/Data/Tournament.swift | 26 ++++++++++++-- .../Extensions/Date+Extensions.swift | 35 +++++++++++++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) diff --git a/PadelClubData/Data/AppSettings.swift b/PadelClubData/Data/AppSettings.swift index 1703b03..fcb1fd5 100644 --- a/PadelClubData/Data/AppSettings.swift +++ b/PadelClubData/Data/AppSettings.swift @@ -29,6 +29,7 @@ final public class AppSettings: MicroStorable { public var nationalCup: Bool public var dayDuration: Int? public var dayPeriod: DayPeriod + public var weekdays: Set public func lastDataSourceDate() -> Date? { guard let lastDataSource else { return nil } @@ -55,6 +56,7 @@ final public class AppSettings: MicroStorable { nationalCup = false dayDuration = nil dayPeriod = .all + weekdays = Set() } public required init() { @@ -70,6 +72,7 @@ final public class AppSettings: MicroStorable { nationalCup = false dayDuration = nil dayPeriod = .all + weekdays = Set() } public required init(from decoder: Decoder) throws { @@ -89,6 +92,7 @@ final public class AppSettings: MicroStorable { nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all + weekdays = try container.decodeIfPresent(Set.self, forKey: ._weekdays) ?? Set() } enum CodingKeys: String, CodingKey { @@ -107,5 +111,6 @@ final public class AppSettings: MicroStorable { case _nationalCup = "nationalCup" case _dayDuration = "dayDuration" case _dayPeriod = "dayPeriod" + case _weekdays = "weekdays" } } diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index bd86494..2cb9df1 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -298,7 +298,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) } - public func readyMatches(playedMatches: [Match]) -> [Match] { + public func readyMatches(playedMatches: [Match], runningMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -306,7 +306,9 @@ final public class GroupStage: BaseGroupStage, SideStorable { print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) + let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id }) + + return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }) } public func finishedMatches(playedMatches: [Match]) -> [Match] { @@ -616,6 +618,16 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func computedStartDate() -> Date? { return _matches().sorted(by: \.computedStartDateForSorting).first?.startDate } + + public func removeAllTeams() { + let teams = teams() + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil + self._matches().forEach({ $0.updateTeamScores() }) + } + tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) + } public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index 39ed5de..637d21f 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -896,6 +896,11 @@ extension Match { return teamIds().contains(id) } + public func containsTeamIds(_ ids: [String]) -> Bool { + let teamIds = teamIds() + return !Set(ids).isDisjoint(with: teamIds) + } + public func containsTeamIndex(_ id: String) -> Bool { matchUp().contains(id) } diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 9c16560..f323fbc 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -88,6 +88,25 @@ final public class Tournament: BaseTournament { return self.tournamentStore?.teamRegistrations.count ?? 0 } + public func deleteGroupStage(_ groupStage: GroupStage) { + groupStage.removeAllTeams() + let index = groupStage.index + self.tournamentStore?.groupStages.delete(instance: groupStage) + self.groupStageCount -= 1 + let groupStages = self.groupStages() + groupStages.filter({ $0.index > index }).forEach { gs in + gs.index -= 1 + } + self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groupStages) + } + + public func addGroupStage() { + let groupStage = GroupStage(tournament: id, index: groupStageCount, size: teamsPerGroupStage, format: groupStageFormat) + self.tournamentStore?.groupStages.addOrUpdate(instance: groupStage) + groupStage.buildMatches(keepExistingMatches: false) + self.groupStageCount += 1 + } + public func groupStages(atStep step: Int = 0) -> [GroupStage] { guard let tournamentStore = self.tournamentStore else { return [] } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } @@ -856,7 +875,7 @@ defer { return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - public static func readyMatches(_ allMatches: [Match]) -> [Match] { + public static func readyMatches(_ allMatches: [Match], runningMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -864,7 +883,10 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) + + let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id }) + + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }).sorted(using: defaultSorting, order: .ascending) } public static func matchesLeft(_ allMatches: [Match]) -> [Match] { diff --git a/PadelClubData/Extensions/Date+Extensions.swift b/PadelClubData/Extensions/Date+Extensions.swift index a9878f0..aa681c4 100644 --- a/PadelClubData/Extensions/Date+Extensions.swift +++ b/PadelClubData/Extensions/Date+Extensions.swift @@ -118,6 +118,14 @@ public extension Date { } } + var nextDay: Date { + return Calendar.current.date(byAdding: .day, value: 1, to: self)! + } + + var weekDay: Int { + Calendar.current.component(.weekday, from: self) + } + func atBeginningOfDay(hourInt: Int = 9) -> Date { Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! } @@ -144,6 +152,28 @@ public extension Date { return weekdays.map { $0.capitalized } }() + static var weekdays: [String] = { + let calendar = Calendar.current + // let weekdays = calendar.shortWeekdaySymbols + + // return weekdays.map { weekday in + // guard let firstLetter = weekday.first else { return "" } + // return String(firstLetter).capitalized + // } + // Adjusted for the different weekday starts + var weekdays = calendar.weekdaySymbols + if firstDayOfWeek > 1 { + for _ in 1.. Date: Thu, 25 Sep 2025 18:17:44 +0200 Subject: [PATCH 11/11] fix issue with ios 26 --- PadelClubData/Data/AppSettings.swift | 5 ----- PadelClubData/ViewModel/PadelRule.swift | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/PadelClubData/Data/AppSettings.swift b/PadelClubData/Data/AppSettings.swift index fcb1fd5..1703b03 100644 --- a/PadelClubData/Data/AppSettings.swift +++ b/PadelClubData/Data/AppSettings.swift @@ -29,7 +29,6 @@ final public class AppSettings: MicroStorable { public var nationalCup: Bool public var dayDuration: Int? public var dayPeriod: DayPeriod - public var weekdays: Set public func lastDataSourceDate() -> Date? { guard let lastDataSource else { return nil } @@ -56,7 +55,6 @@ final public class AppSettings: MicroStorable { nationalCup = false dayDuration = nil dayPeriod = .all - weekdays = Set() } public required init() { @@ -72,7 +70,6 @@ final public class AppSettings: MicroStorable { nationalCup = false dayDuration = nil dayPeriod = .all - weekdays = Set() } public required init(from decoder: Decoder) throws { @@ -92,7 +89,6 @@ final public class AppSettings: MicroStorable { nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all - weekdays = try container.decodeIfPresent(Set.self, forKey: ._weekdays) ?? Set() } enum CodingKeys: String, CodingKey { @@ -111,6 +107,5 @@ final public class AppSettings: MicroStorable { case _nationalCup = "nationalCup" case _dayDuration = "dayDuration" case _dayPeriod = "dayPeriod" - case _weekdays = "weekdays" } } diff --git a/PadelClubData/ViewModel/PadelRule.swift b/PadelClubData/ViewModel/PadelRule.swift index 2c954c8..9df6b83 100644 --- a/PadelClubData/ViewModel/PadelRule.swift +++ b/PadelClubData/ViewModel/PadelRule.swift @@ -25,7 +25,7 @@ enum RankSource: Hashable { } } -public protocol TournamentBuildHolder: Identifiable { +public protocol TournamentBuildHolder: Identifiable, Hashable, Equatable { var id: String { get } var category: TournamentCategory { get } var level: TournamentLevel { get }