Work on subscriptions

multistore
Laurent 2 years ago
parent ae05f8aa4b
commit df9d518407
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 67
      PadelClub/Views/Calling/CallView.swift
  3. 1
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  4. 13
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  5. 2
      PadelClub/Views/Planning/PlanningSettingsView.swift
  6. 37
      PadelClub/Views/Subscription/Guard.swift
  7. 42
      PadelClub/Views/Subscription/OffersHeaderView.swift
  8. 53
      PadelClub/Views/Subscription/PurchaseListView.swift
  9. 6
      PadelClub/Views/Subscription/SubscriptionView.swift
  10. 83
      PadelClub/Views/Tournament/TournamentView.swift

@ -18,6 +18,7 @@
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; }; C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; };
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; }; C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; }; C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; };
C49EF0262BD80AE80077B5AA /* OffersHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* OffersHeaderView.swift */; };
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; }; C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; };
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; }; C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; };
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; }; C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; };
@ -310,6 +311,7 @@
C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = "<group>"; }; C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = "<group>"; };
C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; }; C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; };
C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; }; C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
C49EF0252BD80AE80077B5AA /* OffersHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffersHeaderView.swift; sourceTree = "<group>"; };
C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = "<group>"; }; C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = "<group>"; };
C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; }; C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = "<group>"; }; C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = "<group>"; };
@ -729,6 +731,7 @@
C4A47D8D2B7BBBEC00ADC637 /* StoreManager.swift */, C4A47D8D2B7BBBEC00ADC637 /* StoreManager.swift */,
C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */, C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */,
C49EF0182BD694290077B5AA /* PurchaseListView.swift */, C49EF0182BD694290077B5AA /* PurchaseListView.swift */,
C49EF0252BD80AE80077B5AA /* OffersHeaderView.swift */,
); );
path = Subscription; path = Subscription;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1440,6 +1443,7 @@
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */, FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */, FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */, FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */,
C49EF0262BD80AE80077B5AA /* OffersHeaderView.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */, FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */, FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */,

@ -84,7 +84,7 @@ struct CallView: View {
var body: some View { var body: some View {
let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer" let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer"
HStack { HStack(spacing: 0.0) {
if teams.count == 1 { if teams.count == 1 {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer " + callDate.localizedDate() + " par") Text("Reconvoquer " + callDate.localizedDate() + " par")
@ -120,42 +120,53 @@ struct CallView: View {
.sheet(item: $contactType) { contactType in .sheet(item: $contactType) { contactType in
switch contactType { switch contactType {
case .message(_, let recipients, let body, _): case .message(_, let recipients, let body, _):
MessageComposeView(recipients: recipients, body: body) { result in
switch result { if Guard.main.paymentForNewTournament() != nil {
case .cancelled: MessageComposeView(recipients: recipients, body: body) { result in
_called(true) switch result {
break case .cancelled:
case .failed:
self.sentError = .messageFailed
case .sent:
if networkMonitor.connected == false {
self.sentError = .messageNotSent
} else {
_called(true) _called(true)
break
case .failed:
self.sentError = .messageFailed
case .sent:
if networkMonitor.connected == false {
self.sentError = .messageNotSent
} else {
_called(true)
}
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(showLackOfPlanMessage: true)
} }
case .mail(_, let recipients, let bccRecipients, let body, let subject, _): case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in if Guard.main.paymentForNewTournament() != nil {
switch result {
case .cancelled, .saved: MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
self.contactType = nil switch result {
_called(true) case .cancelled, .saved:
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
self.sentError = .mailNotSent
} else {
_called(true) _called(true)
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil
self.sentError = .mailNotSent
} else {
_called(true)
}
@unknown default:
break
} }
@unknown default:
break
} }
} else {
SubscriptionView(showLackOfPlanMessage: true)
} }
} }
} }

@ -55,6 +55,7 @@ struct ActivityView: View {
NavigationStack(path: $navigation.path) { NavigationStack(path: $navigation.path) {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
List { List {
switch navigation.agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:

@ -17,16 +17,19 @@ struct UmpireView: View {
PurchaseListView() PurchaseListView()
Section {
NavigationLink {
SubscriptionView()
} label: {
Label("Les offres", systemImage: "bookmark.circle.fill")
}
}
NavigationLink { NavigationLink {
MainUserView() MainUserView()
} label: { } label: {
Label("Mon compte", systemImage: "person.circle.fill") Label("Mon compte", systemImage: "person.circle.fill")
} }
NavigationLink {
SubscriptionView()
} label: {
Label("Abonnement", systemImage: "tennisball.circle.fill")
}
if let user = dataStore.user { if let user = dataStore.user {
let currentPlayerData = user.currentPlayerData() let currentPlayerData = user.currentPlayerData()

@ -37,7 +37,9 @@ struct PlanningSettingsView: View {
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
OffersHeaderView()
List { List {
Section { Section {
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate)
LabeledContent { LabeledContent {

@ -141,8 +141,14 @@ import LeStorage
func userFilteredPurchases() -> [StoreKit.Transaction] { func userFilteredPurchases() -> [StoreKit.Transaction] {
return self.purchasedTransactions.filter { transaction in let userTransactions = self.purchasedTransactions.filter { Store.main.currentUserUUID() == $0.appAccountToken }
return Store.main.currentUserUUID() == transaction.appAccountToken
return userTransactions.filter { transaction in
if let expirationDate = transaction.expirationDate {
return expirationDate > Date()
} else {
return true
}
} }
// return self.purchasedTransactions.filter { transaction in // return self.purchasedTransactions.filter { transaction in
@ -201,35 +207,12 @@ import LeStorage
} }
var remainingTournaments: Int { var remainingTournaments: Int {
let subscriptionPayed = DataStore.shared.tournaments.filter { $0.payment?.isSubscription == true } let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == Tournament.TournamentPayment.unit }.count
let unitlyPayed = DataStore.shared.tournaments.count - subscriptionPayed.count
let tournamentCreditCount = self._purchasedTournamentCount() let tournamentCreditCount = self._purchasedTournamentCount()
Logger.log("total count = \(DataStore.shared.tournaments.count), unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ")
return tournamentCreditCount - unitlyPayed return tournamentCreditCount - unitlyPayed
} }
// func purchaseRows(products: [Product]) -> [PurchaseRow] {
//
// var rows: [PurchaseRow] = []
// let userPurchases = self.userFilteredPurchases()
// for userPurchase in userPurchases {
//
// if let item = StoreItem(rawValue: userPurchase.productID),
// let product = products.first(where: { $0.id == item.rawValue } ) {
// switch item {
// case .unit:
// let remainingTournaments = self.remainingTournaments
// if remainingTournaments > 0 {
// rows.append(PurchaseRow(name: product.displayName, item: item, quantity: remainingTournaments))
// }
// default:
// rows.append(PurchaseRow(name: product.displayName, item: item))
// }
// }
// }
//
// return rows
// }
} }
struct PurchaseRow: Identifiable { struct PurchaseRow: Identifiable {

@ -8,9 +8,49 @@
import SwiftUI import SwiftUI
struct OffersHeaderView: View { struct OffersHeaderView: View {
let payment: Tournament.TournamentPayment? = .free
var body: some View { var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) VStack {
if let text = self.text {
Text(text)
.multilineTextAlignment(.center)
.font(.callout)
.padding()
.foregroundColor(self.foregroundColor)
.frame(maxWidth: .infinity)
.background(self.backgroundColor)
}
}
}
var foregroundColor: Color {
switch self.payment {
case .free: return .blue
default: return .red
}
} }
var backgroundColor: Color {
switch self.payment {
case .free: return Color(red: 0.9, green: 0.9, blue: 1.0)
default: return Color(red: 1.0, green: 0.9, blue: 0.9)
}
}
var text: String? {
switch self.payment {
case .free:
return "Nous vous offrons votre premier tournoi ! Convoquez les équipes, créez les poules, le tableau comme vous le souhaitez. \nEnregistrez les résultats de chaque équipes et diffusez les scores en temps réel sur les écrans de votre club !"
case nil:
return "Vous ne disposez pas d'une offre vous permettant de convoquer les joueurs ou de rentrer les résultats des matchs. Vous pouvez consulter les offres dans l'onglet JA."
default:
return nil
}
}
} }
#Preview { #Preview {

@ -25,7 +25,7 @@ class PurchaseManager: ObservableObject {
Task { Task {
do { do {
self._products = try await Product.products(for: identifiers) self._products = try await Product.products(for: identifiers)
self._buildRows() self._buildRowsOnMainThread()
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -38,33 +38,46 @@ class PurchaseManager: ObservableObject {
} }
self._purchases.removeAll() self._purchases.removeAll()
self._purchases.append(contentsOf: collection) self._purchases.append(contentsOf: collection)
self._buildRows() self._buildRowsOnMainThread()
} }
fileprivate func _buildRows() { fileprivate func _buildRowsOnMainThread() {
DispatchQueue.main.async { DispatchQueue.main.async {
self._buildRows()
}
var rows: [PurchaseRow] = [] }
let userPurchases: [StoreKit.Transaction] = Guard.main.userFilteredPurchases()
for userPurchase in userPurchases { fileprivate func _buildRows() {
if let item = StoreItem(rawValue: userPurchase.productID), var rows: [PurchaseRow] = []
let product = self._products.first(where: { $0.id == item.rawValue } ) { let userPurchases: [StoreKit.Transaction] = Guard.main.userFilteredPurchases()
switch item {
case .unit: // Subscriptions
let remainingTournaments = Guard.main.remainingTournaments for userPurchase in userPurchases {
if remainingTournaments > 0 {
rows.append(PurchaseRow(name: product.displayName, item: item, quantity: remainingTournaments)) if let item = StoreItem(rawValue: userPurchase.productID),
} let product = self._products.first(where: { $0.id == item.rawValue } ) {
default: switch item {
rows.append(PurchaseRow(name: product.displayName, item: item)) case .fivePerMonth, .monthlyUnlimited:
} rows.append(PurchaseRow(name: product.displayName, item: item))
case .unit:
break
} }
} }
self.purchaseRows = rows
} }
// Units
let remainingTournaments = Guard.main.remainingTournaments
if remainingTournaments > 0 {
let unitItem: StoreItem = StoreItem.unit
if let product = self._products.first(where: { $0.id == unitItem.rawValue } ) {
rows.append(PurchaseRow(name: product.displayName, item: unitItem, quantity: remainingTournaments))
}
}
self.purchaseRows = rows
} }
} }
@ -104,7 +117,7 @@ struct PurchaseView: View {
Spacer() Spacer()
if let quantity = purchaseRow.quantity { if let quantity = purchaseRow.quantity {
let remaining = Guard.main.remainingTournaments let remaining = Guard.main.remainingTournaments
Text("\(remaining) / \(quantity.formatted())") Text("\(remaining)")
} }
} }
} }

@ -104,6 +104,7 @@ struct SubscriptionView: View {
@ObservedObject var model: SubscriptionModel = SubscriptionModel() @ObservedObject var model: SubscriptionModel = SubscriptionModel()
var showLackOfPlanMessage: Bool = false
@State var isRestoring: Bool = false @State var isRestoring: Bool = false
@State var showLoginView: Bool = false @State var showLoginView: Bool = false
@ -118,6 +119,11 @@ struct SubscriptionView: View {
} else { } else {
Form { Form {
if self.showLackOfPlanMessage {
Text("Vous ne disposez malheureusement pas d'offre pour continuer votre tournoi. Voici ce que nous proposons:")
}
if self.model.products.count > 0 { if self.model.products.count > 0 {
Section { Section {

@ -22,51 +22,58 @@ struct TournamentView: View {
} }
var body: some View { var body: some View {
List {
VStack(spacing: 0.0) {
// if tournament.missingUnrankedValue() {
// Button("update NC") { OffersHeaderView()
// tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate)
// tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate) List {
// try? dataStore.tournaments.addOrUpdate(instance: tournament)
// }
// } // if tournament.missingUnrankedValue() {
// // Button("update NC") {
// // tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate)
Section { // tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate)
NavigationLink(value: Screen.inscription) { // try? dataStore.tournaments.addOrUpdate(instance: tournament)
LabeledContent { // }
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) // }
.foregroundStyle(.master) //
} label: { //
Text("Gestion des inscriptions") Section {
if let closedRegistrationDate = tournament.closedRegistrationDate { NavigationLink(value: Screen.inscription) {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted())
.foregroundStyle(.master)
} label: {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened))
}
} }
} }
} if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { LabeledContent {
LabeledContent { Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened))
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) .foregroundStyle(.master)
.foregroundStyle(.master) } label: {
} label: { Text("Date limite")
Text("Date limite") }
}
if endOfInscriptionDate < Date() { if endOfInscriptionDate < Date() {
RowButtonView("Clôturer les inscriptions") { RowButtonView("Clôturer les inscriptions") {
tournament.lockRegistration() tournament.lockRegistration()
_save() _save()
}
} }
} }
} }
}
switch tournament.state() { switch tournament.state() {
case .initial: case .initial:
TournamentInitView() TournamentInitView()
case .build: case .build:
TournamentRunningView(tournament: tournament) TournamentRunningView(tournament: tournament)
}
} }
} }
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)

Loading…
Cancel
Save