More work on subscriptions and fixes

multistore
Laurent 2 years ago
parent 56553329a1
commit e9ec8111b8
  1. 12
      PadelClub/Assets.xcassets/AccentColor.colorset/Contents.json
  2. 52
      PadelClub/Data/DataStore.swift
  3. 6
      PadelClub/Data/User.swift
  4. 9
      PadelClub/Launch Screen.storyboard
  5. 4
      PadelClub/Views/Match/MatchDetailView.swift
  6. 7
      PadelClub/Views/Subscription/Guard.swift
  7. 2
      PadelClub/Views/Subscription/PurchaseListView.swift
  8. 16
      PadelClub/Views/Subscription/SubscriptionView.swift
  9. 2
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  10. 2
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift
  11. 3
      PadelClub/Views/Tournament/TournamentView.swift
  12. 2
      PadelClub/Views/User/AccountView.swift
  13. 2
      PadelClub/Views/User/LoginView.swift
  14. 56
      PadelClubTests/PaymentTests.swift

@ -1,11 +1,23 @@
{ {
"colors" : [ "colors" : [
{ {
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.573",
"red" : "0.953"
}
},
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"localizable" : true
} }
} }

@ -28,7 +28,9 @@ class DataStore: ObservableObject {
fileprivate(set) var monthData: StoredCollection<MonthData> fileprivate(set) var monthData: StoredCollection<MonthData>
fileprivate(set) var dateIntervals: StoredCollection<DateInterval> fileprivate(set) var dateIntervals: StoredCollection<DateInterval>
fileprivate var _userStorage: OptionalStorage<User> = OptionalStorage<User>(fileName: "user.json") fileprivate(set) var userStorage: StoredObject<User>
// fileprivate var _userStorage: OptionalStorage<User> = OptionalStorage<User>(fileName: "user.json")
fileprivate var _appSettingsStorage: MicroStorage<AppSettings> = MicroStorage() fileprivate var _appSettingsStorage: MicroStorage<AppSettings> = MicroStorage()
var appSettings: AppSettings { var appSettings: AppSettings {
@ -51,11 +53,18 @@ class DataStore: ObservableObject {
} }
var user: User? { var user: User? {
return self._userStorage.item return self.userStorage.item()
// return self._userStorage.item
} }
func setUser(_ user: User?) { func setUser(_ user: User) {
self._userStorage.item = user do {
try self.userStorage.setItem(user)
self._loadCollections()
} catch {
Logger.error(error)
}
// self._userStorage.item = user
} }
init() { init() {
@ -68,18 +77,21 @@ class DataStore: ObservableObject {
// store.addMigration(Migration<TournamentV2, Tournament>(version: 3)) // store.addMigration(Migration<TournamentV2, Tournament>(version: 3))
let indexed : Bool = true let indexed : Bool = true
self.clubs = store.registerCollection(synchronized: false, indexed: indexed) let synchronized : Bool = false
self.courts = store.registerCollection(synchronized: false, indexed: indexed) self.clubs = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.tournaments = store.registerCollection(synchronized: false, indexed: indexed) self.courts = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.events = store.registerCollection(synchronized: false, indexed: indexed) self.tournaments = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.groupStages = store.registerCollection(synchronized: false, indexed: indexed) self.events = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.teamScores = store.registerCollection(synchronized: false, indexed: indexed) self.groupStages = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.teamRegistrations = store.registerCollection(synchronized: false, indexed: indexed) self.teamScores = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.playerRegistrations = store.registerCollection(synchronized: false, indexed: indexed) self.teamRegistrations = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.rounds = store.registerCollection(synchronized: false, indexed: indexed) self.playerRegistrations = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.matches = store.registerCollection(synchronized: false, indexed: indexed) self.rounds = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.monthData = store.registerCollection(synchronized: false, indexed: indexed) self.matches = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.dateIntervals = store.registerCollection(synchronized: false, indexed: indexed) self.monthData = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.dateIntervals = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.userStorage = store.registerObject(synchronized: synchronized)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil)
@ -108,4 +120,12 @@ class DataStore: ObservableObject {
return .none return .none
} }
func disconnect() {
Store.main.disconnect(resetAll: true)
}
fileprivate func _loadCollections() {
Store.main.loadCollections()
}
} }

@ -15,7 +15,11 @@ enum UserRight: Int, Codable {
} }
@Observable @Observable
class User: UserBase { class User: UserBase, Storable {
static func resourceName() -> String { "users" }
func deleteDependencies() throws { }
public var id: String = Store.randomId() public var id: String = Store.randomId()
public var username: String public var username: String

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Named colors" minToolsVersion="9.0"/> <capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -18,7 +17,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="PadelClub_logo_fondfonce_transparent" translatesAutoresizingMaskIntoConstraints="NO" id="Lsd-2D-TDo"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="PadelClub_logo_fondfonce_transparent" translatesAutoresizingMaskIntoConstraints="NO" id="Lsd-2D-TDo">
<rect key="frame" x="64" y="-628.66666666666663" width="265" height="2134.3333333333335"/> <rect key="frame" x="90" y="-628.66666666666663" width="213" height="2134.3333333333335"/>
</imageView> </imageView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/> <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
@ -26,7 +25,7 @@
<color key="tintColor" name="AccentColor"/> <color key="tintColor" name="AccentColor"/>
<constraints> <constraints>
<constraint firstItem="Lsd-2D-TDo" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="7Tm-4j-KPz"/> <constraint firstItem="Lsd-2D-TDo" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="7Tm-4j-KPz"/>
<constraint firstItem="Lsd-2D-TDo" firstAttribute="width" secondItem="Ze5-6b-2t3" secondAttribute="height" multiplier="265:852" id="Nhb-n4-HGG"/> <constraint firstItem="Lsd-2D-TDo" firstAttribute="width" secondItem="Ze5-6b-2t3" secondAttribute="height" multiplier="1:4" id="Nhb-n4-HGG"/>
<constraint firstItem="Lsd-2D-TDo" firstAttribute="centerY" secondItem="Bcu-3y-fUS" secondAttribute="centerY" id="xOG-dI-NIb"/> <constraint firstItem="Lsd-2D-TDo" firstAttribute="centerY" secondItem="Bcu-3y-fUS" secondAttribute="centerY" id="xOG-dI-NIb"/>
</constraints> </constraints>
</view> </view>

@ -181,7 +181,9 @@ struct MatchDetailView: View {
MatchTeamDetailView(match: match).tint(.master) MatchTeamDetailView(match: match).tint(.master)
} }
.sheet(isPresented: self.$showSubscriptionView, content: { .sheet(isPresented: self.$showSubscriptionView, content: {
SubscriptionView(showLackOfPlanMessage: true) NavigationStack {
SubscriptionView(showLackOfPlanMessage: true)
}
}) })
.sheet(item: $scoreType, onDismiss: { .sheet(item: $scoreType, onDismiss: {
if match.hasEnded() { if match.hasEnded() {

@ -185,13 +185,12 @@ import LeStorage
} }
return nil return nil
default: default:
// let subscriptionPayed = DataStore.shared.tournaments.filter { $0.payment?.isSubscription == true } let freelyPayed = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count
if freelyPayed < 1 {
let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == .unit && $0.isCanceled == false }.count
if unitlyPayed == 0 {
return Tournament.TournamentPayment.free return Tournament.TournamentPayment.free
} }
let tournamentCreditCount = self._purchasedTournamentCount() let tournamentCreditCount = self._purchasedTournamentCount()
let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == .unit && $0.isCanceled == false }.count
if tournamentCreditCount > unitlyPayed { if tournamentCreditCount > unitlyPayed {
return Tournament.TournamentPayment.unit return Tournament.TournamentPayment.unit
} }

@ -115,7 +115,7 @@ struct PurchaseView: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text(self.purchaseRow.name) Text(self.purchaseRow.name)
Spacer() Spacer()
if let quantity = purchaseRow.quantity { if let _ = purchaseRow.quantity {
let remaining = Guard.main.remainingTournaments let remaining = Guard.main.remainingTournaments
Text("\(remaining)") Text("\(remaining)")
} }

@ -51,6 +51,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
} }
@Published var products: [Product] = [] @Published var products: [Product] = []
@Published var totalPrice: String = "" @Published var totalPrice: String = ""
@State var showSuccessfulPurchaseView: Bool = false
func load() { func load() {
self.isLoading = true self.isLoading = true
@ -77,7 +78,9 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
} }
Task { Task {
if product.item.isConsumable { if product.item.isConsumable {
let _ = try await self.storeManager?.purchase(product, quantity: self.quantity) if let result = try await self.storeManager?.purchase(product, quantity: self.quantity) {
self.showSuccessfulPurchaseView = true
}
} else { } else {
let _ = try await self.storeManager?.purchase(product) let _ = try await self.storeManager?.purchase(product)
} }
@ -121,7 +124,12 @@ struct SubscriptionView: View {
Form { Form {
if self.showLackOfPlanMessage { if self.showLackOfPlanMessage {
Text("Vous ne disposez malheureusement pas d'offre pour continuer votre tournoi. Voici ce que nous proposons:") HStack {
Image(systemName: "exclamationmark.bubble.fill").foregroundStyle(Color.accentColor)
.font(.title)
Text("Vous ne disposez malheureusement plus d'offre pour continuer votre tournoi. Voici ce que nous proposons:")
.fontWeight(.semibold)
}
} }
if self.model.products.count > 0 { if self.model.products.count > 0 {
@ -196,7 +204,6 @@ struct SubscriptionView: View {
fileprivate func _restore() { fileprivate func _restore() {
Task { Task {
do { do {
self.isRestoring = true self.isRestoring = true
try await Guard.main.refreshPurchasedAppleProducts() try await Guard.main.refreshPurchasedAppleProducts()
@ -205,7 +212,6 @@ struct SubscriptionView: View {
self.isRestoring = false self.isRestoring = false
Logger.error(error) Logger.error(error)
} }
} }
} }
@ -292,6 +298,6 @@ struct SubscriptionFooterView: View {
#Preview { #Preview {
NavigationStack { NavigationStack {
SubscriptionView() SubscriptionView(showLackOfPlanMessage: true)
} }
} }

@ -26,7 +26,7 @@ struct BroadcastView: View {
List { List {
Section { Section {
Toggle(isOn: $tournament.isPrivate) { Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privée") Text("Tournoi privé")
} }
} footer: { } footer: {
Text("Le tournoi sera masqué sur le site \(URLs.main.rawValue)") Text("Le tournoi sera masqué sur le site \(URLs.main.rawValue)")

@ -68,7 +68,7 @@ struct TournamentStatusView: View {
Section { Section {
Toggle(isOn: $tournament.isPrivate) { Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privée") Text("Tournoi privé")
} }
} footer: { } footer: {
Text("Le tournoi sera masqué sur le site padelclub.app") Text("Le tournoi sera masqué sur le site padelclub.app")

@ -157,6 +157,9 @@ struct TournamentView: View {
} }
} }
} }
.onAppear {
Logger.log("Payment = \(String(describing: self.tournament.payment)), canceled = \(self.tournament.isCanceled)")
}
} }
private func _save() { private func _save() {

@ -19,7 +19,7 @@ struct AccountView: View {
ChangePasswordView() ChangePasswordView()
} }
Button("Disconnect") { Button("Disconnect") {
Store.main.disconnect() DataStore.shared.disconnect()
handler() handler()
} }
}.navigationTitle(user.username) }.navigationTitle(user.username)

@ -13,7 +13,7 @@ struct LoginView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State var username: String = "laurent" @State var username: String = "laurent"
@State var password: String = "staxstax" @State var password: String = "StaxKikoo12"
@State var isLoading: Bool = false @State var isLoading: Bool = false
@State var showEmailPopup: Bool = false @State var showEmailPopup: Bool = false

@ -21,14 +21,30 @@ final class PaymentTests: XCTestCase {
func testPayments() throws { func testPayments() throws {
let tournament = Tournament.fake() let tournament = Tournament.fake()
tournament.setPayment(.free) do {
assert(tournament.decryptPayment() == .free) tournament.payment = .free
tournament.setPayment(.subscriptionUnit) var encoded = try JSONEncoder().encode(tournament)
assert(tournament.decryptPayment() == .subscriptionUnit) var decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
tournament.setPayment(.unit) assert(decoded.payment == .free)
assert(tournament.decryptPayment() == .unit)
tournament.setPayment(.unlimited) tournament.payment = .subscriptionUnit
assert(tournament.decryptPayment() == .unlimited) encoded = try JSONEncoder().encode(tournament)
decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
assert(decoded.payment == .subscriptionUnit)
tournament.payment = .unit
encoded = try JSONEncoder().encode(tournament)
decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
assert(decoded.payment == .unit)
tournament.payment = .unlimited
encoded = try JSONEncoder().encode(tournament)
decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
assert(decoded.payment == .unlimited)
} catch {
assertionFailure(error.localizedDescription)
}
} }
@ -36,10 +52,26 @@ final class PaymentTests: XCTestCase {
let tournament = Tournament.fake() let tournament = Tournament.fake()
tournament.setCanceled(true) do {
assert(tournament.decryptCanceled() == true) tournament.isCanceled = false
tournament.setCanceled(false) var encoded = try JSONEncoder().encode(tournament)
assert(tournament.decryptCanceled() == false) var decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
assert(decoded.isCanceled == false)
tournament.isCanceled = true
encoded = try JSONEncoder().encode(tournament)
decoded = try JSONDecoder().decode(Tournament.self, from: encoded)
assert(decoded.isCanceled == true)
} catch {
assertionFailure(error.localizedDescription)
}
// tournament.setCanceled(true)
// assert(tournament.decryptCanceled() == true)
// tournament.setCanceled(false)
// assert(tournament.decryptCanceled() == false)
} }

Loading…
Cancel
Save