Adds token exempted method for Storable + tests

multistore
Laurent 2 years ago
parent efafff9eff
commit c913cad7d9
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Assets.xcassets/MasterColor.colorset/Contents.json
  3. 4
      PadelClub/Data/Club.swift
  4. 2
      PadelClub/Data/Court.swift
  5. 2
      PadelClub/Data/DateInterval.swift
  6. 2
      PadelClub/Data/Event.swift
  7. 2
      PadelClub/Data/GroupStage.swift
  8. 2
      PadelClub/Data/Match.swift
  9. 2
      PadelClub/Data/MonthData.swift
  10. 2
      PadelClub/Data/PlayerRegistration.swift
  11. 2
      PadelClub/Data/Round.swift
  12. 2
      PadelClub/Data/TeamRegistration.swift
  13. 2
      PadelClub/Data/TeamScore.swift
  14. 2
      PadelClub/Data/Tournament.swift
  15. 2
      PadelClub/Data/User.swift
  16. 13
      PadelClub/Views/Subscription/Guard.swift
  17. 5
      PadelClub/Views/Subscription/Purchase.swift
  18. 12
      PadelClub/Views/Subscription/StoreItem.swift
  19. 71
      PadelClub/Views/Subscription/SubscriptionView.swift
  20. 58
      PadelClubTests/TokenExemptionTests.swift

@ -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 = "<group>"; };
C411C9C82BF219CB003017AD /* UserDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataTests.swift; sourceTree = "<group>"; };
C411C9CC2BF21DAF003017AD /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenExemptionTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C425D4042B6D249E002A7B48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -652,6 +654,7 @@
C49EF0412BE23BF50077B5AA /* PaymentTests.swift */,
C411C9C22BEBA453003017AD /* ServerDataTests.swift */,
C411C9C82BF219CB003017AD /* UserDataTests.swift */,
C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */,
);
path = PadelClubTests;
sourceTree = "<group>";
@ -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 */,

@ -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"

@ -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
}

@ -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

@ -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

@ -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?

@ -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

@ -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

@ -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

@ -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?

@ -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

@ -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

@ -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

@ -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?

@ -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 { }

@ -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
}

@ -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

@ -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"

@ -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)
}
}

@ -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
}
}
Loading…
Cancel
Save