Work on subscriptions

multistore
Laurent 2 years ago
parent ae05f8aa4b
commit df9d518407
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 13
      PadelClub/Views/Calling/CallView.swift
  3. 1
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  4. 11
      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. 33
      PadelClub/Views/Subscription/PurchaseListView.swift
  9. 6
      PadelClub/Views/Subscription/SubscriptionView.swift
  10. 7
      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,6 +120,8 @@ 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, _):
if Guard.main.paymentForNewTournament() != nil {
MessageComposeView(recipients: recipients, body: body) { result in MessageComposeView(recipients: recipients, body: body) { result in
switch result { switch result {
case .cancelled: case .cancelled:
@ -137,7 +139,13 @@ struct CallView: View {
break 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, _):
if Guard.main.paymentForNewTournament() != nil {
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
switch result { switch result {
case .cancelled, .saved: case .cancelled, .saved:
@ -157,6 +165,9 @@ struct CallView: View {
break 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,15 +17,18 @@ struct UmpireView: View {
PurchaseListView() PurchaseListView()
Section {
NavigationLink { NavigationLink {
MainUserView() SubscriptionView()
} label: { } label: {
Label("Mon compte", systemImage: "person.circle.fill") Label("Les offres", systemImage: "bookmark.circle.fill")
}
} }
NavigationLink { NavigationLink {
SubscriptionView() MainUserView()
} label: { } label: {
Label("Abonnement", systemImage: "tennisball.circle.fill") Label("Mon compte", systemImage: "person.circle.fill")
} }
if let user = dataStore.user { if let user = dataStore.user {

@ -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()
}
}
fileprivate func _buildRows() {
var rows: [PurchaseRow] = [] var rows: [PurchaseRow] = []
let userPurchases: [StoreKit.Transaction] = Guard.main.userFilteredPurchases() let userPurchases: [StoreKit.Transaction] = Guard.main.userFilteredPurchases()
// Subscriptions
for userPurchase in userPurchases { for userPurchase in userPurchases {
if let item = StoreItem(rawValue: userPurchase.productID), if let item = StoreItem(rawValue: userPurchase.productID),
let product = self._products.first(where: { $0.id == item.rawValue } ) { let product = self._products.first(where: { $0.id == item.rawValue } ) {
switch item { switch item {
case .fivePerMonth, .monthlyUnlimited:
rows.append(PurchaseRow(name: product.displayName, item: item))
case .unit: case .unit:
let remainingTournaments = Guard.main.remainingTournaments break
if remainingTournaments > 0 {
rows.append(PurchaseRow(name: product.displayName, item: item, quantity: remainingTournaments))
} }
default:
rows.append(PurchaseRow(name: product.displayName, item: item))
} }
} }
// 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
} }
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,8 +22,14 @@ struct TournamentView: View {
} }
var body: some View { var body: some View {
VStack(spacing: 0.0) {
OffersHeaderView()
List { List {
// if tournament.missingUnrankedValue() { // if tournament.missingUnrankedValue() {
// Button("update NC") { // Button("update NC") {
// tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate) // tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate)
@ -69,6 +75,7 @@ struct TournamentView: View {
TournamentRunningView(tournament: tournament) TournamentRunningView(tournament: tournament)
} }
} }
}
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationDestination(for: Screen.self, destination: { screen in .navigationDestination(for: Screen.self, destination: { screen in
Group { Group {

Loading…
Cancel
Save