diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ad789ed..736b019 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C22BEBA453003017AD /* ServerDataTests.swift */; }; C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9C82BF219CB003017AD /* UserDataTests.swift */; }; + C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */; }; C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4002B6D249D002A7B48 /* PadelClubApp.swift */; }; C425D4052B6D249E002A7B48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; C425D4082B6D249E002A7B48 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; }; @@ -305,6 +306,7 @@ C411C9C22BEBA453003017AD /* ServerDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDataTests.swift; sourceTree = ""; }; C411C9C82BF219CB003017AD /* UserDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataTests.swift; sourceTree = ""; }; C411C9CC2BF21DAF003017AD /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenExemptionTests.swift; sourceTree = ""; }; C425D3FD2B6D249D002A7B48 /* PadelClub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PadelClub.app; sourceTree = BUILT_PRODUCTS_DIR; }; C425D4002B6D249D002A7B48 /* PadelClubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubApp.swift; sourceTree = ""; }; C425D4042B6D249E002A7B48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -652,6 +654,7 @@ C49EF0412BE23BF50077B5AA /* PaymentTests.swift */, C411C9C22BEBA453003017AD /* ServerDataTests.swift */, C411C9C82BF219CB003017AD /* UserDataTests.swift */, + C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */, ); path = PadelClubTests; sourceTree = ""; @@ -1643,6 +1646,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */, C49EF0422BE23BF50077B5AA /* PaymentTests.swift in Sources */, C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */, C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */, diff --git a/PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json b/PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json index 210d225..951bef4 100644 --- a/PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json +++ b/PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json @@ -42,8 +42,8 @@ "components" : { "alpha" : "1.000", "blue" : "0.000", - "green" : "0.827", - "red" : "0.996" + "green" : "0.573", + "red" : "0.953" } }, "idiom" : "universal" diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 8439e10..a6bcc5b 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -13,8 +13,8 @@ import LeStorage class Club : ModelObject, Storable, Hashable { static func resourceName() -> String { return "clubs" } - static func requestsRequiresToken() -> Bool { return false } - + static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] } + static func == (lhs: Club, rhs: Club) -> Bool { lhs.id == rhs.id } diff --git a/PadelClub/Data/Court.swift b/PadelClub/Data/Court.swift index 0ace2b0..39bf025 100644 --- a/PadelClub/Data/Court.swift +++ b/PadelClub/Data/Court.swift @@ -12,7 +12,7 @@ import LeStorage @Observable class Court : ModelObject, Storable, Hashable { static func resourceName() -> String { return "courts" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func == (lhs: Court, rhs: Court) -> Bool { lhs.id == rhs.id diff --git a/PadelClub/Data/DateInterval.swift b/PadelClub/Data/DateInterval.swift index ebd1002..a0f2d66 100644 --- a/PadelClub/Data/DateInterval.swift +++ b/PadelClub/Data/DateInterval.swift @@ -12,7 +12,7 @@ import LeStorage @Observable class DateInterval: ModelObject, Storable { static func resourceName() -> String { return "date-intervals" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var event: String diff --git a/PadelClub/Data/Event.swift b/PadelClub/Data/Event.swift index 2e9e5a8..44c89e4 100644 --- a/PadelClub/Data/Event.swift +++ b/PadelClub/Data/Event.swift @@ -12,7 +12,7 @@ import SwiftUI @Observable class Event: ModelObject, Storable { static func resourceName() -> String { return "events" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var creator: String? diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 39f7428..50c361d 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -13,7 +13,7 @@ import SwiftUI @Observable class GroupStage: ModelObject, Storable { static func resourceName() -> String { "group-stages" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var tournament: String diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index c189f4b..3f36e5a 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -11,7 +11,7 @@ import LeStorage @Observable class Match: ModelObject, Storable { static func resourceName() -> String { "matches" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var byeState: Bool = false diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift index 4820548..4ab9ff3 100644 --- a/PadelClub/Data/MonthData.swift +++ b/PadelClub/Data/MonthData.swift @@ -13,7 +13,7 @@ import LeStorage class MonthData : ModelObject, Storable { static func resourceName() -> String { return "month-data" } - static func requestsRequiresToken() -> Bool { return false } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } private(set) var id: String = Store.randomId() private(set) var monthKey: String diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index e2a8aa2..0fabc2c 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -11,7 +11,7 @@ import LeStorage @Observable class PlayerRegistration: ModelObject, Storable { static func resourceName() -> String { "player-registrations" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var teamRegistration: String? diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 323da8d..f5b38e8 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -12,7 +12,7 @@ import SwiftUI @Observable class Round: ModelObject, Storable { static func resourceName() -> String { "rounds" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var tournament: String diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 562680f..f3da644 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -11,7 +11,7 @@ import LeStorage @Observable class TeamRegistration: ModelObject, Storable { static func resourceName() -> String { "team-registrations" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var tournament: String diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 5f36e1d..bba788f 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -12,7 +12,7 @@ import LeStorage class TeamScore: ModelObject, Storable { static func resourceName() -> String { "team-scores" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var match: String diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b9a5a2a..3cd765a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -11,7 +11,7 @@ import LeStorage @Observable class Tournament : ModelObject, Storable { static func resourceName() -> String { "tournaments" } - static func requestsRequiresToken() -> Bool { return true } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var event: String? diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/User.swift index 88fec23..d945539 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/User.swift @@ -18,7 +18,7 @@ enum UserRight: Int, Codable { class User: ModelObject, UserBase, Storable { static func resourceName() -> String { "users" } - static func requestsRequiresToken() -> Bool { return false } + static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] } // func deleteDependencies() throws { } diff --git a/PadelClub/Views/Subscription/Guard.swift b/PadelClub/Views/Subscription/Guard.swift index f8c6854..528e9e0 100644 --- a/PadelClub/Views/Subscription/Guard.swift +++ b/PadelClub/Views/Subscription/Guard.swift @@ -134,8 +134,12 @@ import LeStorage } func userFilteredPurchases() -> [StoreKit.Transaction] { + Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)") + guard let currentUserUUID = Store.main.currentUserUUID else { + return [] + } - let userTransactions = self.purchasedTransactions.filter { Store.main.mandatoryUserUUID() == $0.appAccountToken } + let userTransactions = self.purchasedTransactions.filter { currentUserUUID == $0.appAccountToken } return userTransactions.filter { transaction in if let expirationDate = transaction.expirationDate { @@ -145,9 +149,6 @@ import LeStorage } } -// return self.purchasedTransactions.filter { transaction in -// return self.purchases.contains(where: { $0.identifier == transaction.id } ) -// } } /// Update best plan by filtering Apple purchases with registered purchases by the user @@ -202,7 +203,9 @@ import LeStorage var remainingTournaments: Int { let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == Tournament.TournamentPayment.unit }.count let tournamentCreditCount = self._purchasedTournamentCount() - Logger.log("total count = \(DataStore.shared.tournaments.count), unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ") +// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count + + Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ") return tournamentCreditCount - unitlyPayed } diff --git a/PadelClub/Views/Subscription/Purchase.swift b/PadelClub/Views/Subscription/Purchase.swift index aeab9aa..6954488 100644 --- a/PadelClub/Views/Subscription/Purchase.swift +++ b/PadelClub/Views/Subscription/Purchase.swift @@ -9,9 +9,10 @@ import Foundation import LeStorage class Purchase: ModelObject, Storable { + static func resourceName() -> String { return "purchases" } - static func requestsRequiresToken() -> Bool { return true } - + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + var id: String = Store.randomId() var user: String diff --git a/PadelClub/Views/Subscription/StoreItem.swift b/PadelClub/Views/Subscription/StoreItem.swift index 95d034d..8f1a7c1 100644 --- a/PadelClub/Views/Subscription/StoreItem.swift +++ b/PadelClub/Views/Subscription/StoreItem.swift @@ -1,8 +1,8 @@ // -// StorePlan.swift -// Poker Analytics 6 +// StoreItem.swift +// Padel Club // -// Created by Laurent Morvillier on 22/04/2022. +// Created by Laurent Morvillier on 22/04/2024. // import Foundation @@ -16,12 +16,6 @@ enum StoreItem: String, Identifiable, CaseIterable { var id: String { return self.rawValue } -// var title: String { return "Tournoi illimités" } -// -// var formattedPrice: String { return "119.99€ / an" } -// -// var price: Double { return 19.99 } - var systemImage: String { switch self { case .monthlyUnlimited: return "infinity.circle.fill" diff --git a/PadelClub/Views/Subscription/SubscriptionView.swift b/PadelClub/Views/Subscription/SubscriptionView.swift index eb4a17d..d9b29f8 100644 --- a/PadelClub/Views/Subscription/SubscriptionView.swift +++ b/PadelClub/Views/Subscription/SubscriptionView.swift @@ -73,6 +73,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate { } func purchase() { + Logger.log("start purchase...") guard let product: Product = self.selectedProduct else { return } @@ -121,7 +122,7 @@ struct SubscriptionView: View { } } else { - Form { + List { if self.showLackOfPlanMessage { HStack { @@ -136,44 +137,60 @@ struct SubscriptionView: View { Section { - List { ForEach(self.model.products) { product in let isSelected = self.model.selectedProduct == product ProductView(product: product, quantity: self.$model.quantity, selected: isSelected) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + .tint(Color.master) + .listRowBackground(Color.clear) + .onTapGesture { self.model.selectedProduct = product } } - } } header: { - Text("Les offres") + Text("Sélectionnez une offre") } - Section { - Button { - if Store.main.hasToken() { - self._purchase() - } else { - self.showLoginView = true - } - } label: { - HStack { - Text("Acheter") - if let _ = self.model.selectedProduct { + if let product = self.model.selectedProduct { + + Section { + + Button { + if Store.main.hasToken() { + self._purchase() + } else { + self.showLoginView = true + } + } label: { + HStack { + Text("Acheter") Spacer() Text(self.model.totalPrice) - } + }.padding(8.0) + .fontWeight(.bold) + } + .buttonStyle(.borderedProminent) + .tint(.orange) + .listRowBackground(Color.clear) + + } footer: { + if product.item.isConsumable == false { + SubscriptionFooterView() + .foregroundStyle(Color(white: 0.8)) } - } - } footer: { - if self.model.selectedProduct?.item.isConsumable == false { - SubscriptionFooterView() } } + } } + .listStyle(.grouped) + .scrollContentBackground(.hidden) + .background(.logoBackground) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { if self.isRestoring { @@ -187,6 +204,7 @@ struct SubscriptionView: View { } } } + .preferredColorScheme(.dark) .navigationTitle("Abonnements") .onAppear { self._load() @@ -226,8 +244,8 @@ struct ProductView: View { var body: some View { HStack { Image(systemName: self._image) - .font(.title) - .foregroundColor(.accentColor) + .font(.system(size: 36.0)) + .foregroundColor(.orange) VStack(alignment: .leading) { Text(product.displayName) Text(product.formattedPrice) @@ -241,9 +259,11 @@ struct ProductView: View { Image(systemName: "checkmark") .foregroundColor(.accentColor) } - }.contentShape(.rect) + } + .contentShape(.rect) + } - + fileprivate var _item: StoreItem? { return StoreItem(rawValue: self.product.id) } @@ -293,11 +313,12 @@ struct SubscriptionNoProductView: View { struct SubscriptionFooterView: View { var body: some View { Text("Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte Apple.\n- L’abonnement est renouvelé automatiquement chaque mois, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l'offre gratuite, si souscrite, sera abandonnée lorsque l'utilisateur s'abonnera, dans les cas applicables.") + .padding(.top) } } #Preview { NavigationStack { - SubscriptionView(showLackOfPlanMessage: true) + SubscriptionView(showLackOfPlanMessage: false) } } diff --git a/PadelClubTests/TokenExemptionTests.swift b/PadelClubTests/TokenExemptionTests.swift new file mode 100644 index 0000000..1d28abd --- /dev/null +++ b/PadelClubTests/TokenExemptionTests.swift @@ -0,0 +1,58 @@ +// +// TokenExemptionTests.swift +// PadelClubTests +// +// Created by Laurent Morvillier on 14/05/2024. +// + + +import XCTest +import LeStorage +@testable import PadelClub + +final class TokenExemptionTests: XCTestCase { + + let username: String = "test" + let password: String = "MyPass1234--" + + override func setUpWithError() throws { + Store.main.synchronizationApiURL = "http://127.0.0.1:8000/api/" + Store.main.disconnect() + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testClubCreation() async throws { + + let user = try await self.login() + Store.main.disconnect() + + let club: Club = Club(creator: user.id, name: "mon club 2", acronym: "MC", phone: "132", code: "456", address: "l'adresse", city: "la ville", zipCode: "13131", latitude: 13.11111, longitude: 1.121212) + + let c = try await Store.main.service().post(club) + assert(c.id == club.id) + + do { + _ = try await Store.main.service().put(club) + assertionFailure("the request above should fail without an authenticated user") + } catch { + // good stuff + } + + let _ = try await self.login() + club.creator = user.id + + let uc = try await Store.main.service().put(club) + assert(uc.creator == user.id) + + } + + func login() async throws -> User { +// print("LOGIN!") + let user: User = try await Store.main.service().login(username: self.username, password: self.password) + return user + } + +}