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. 8
      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 */; };
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.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 */; };
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.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>"; };
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>"; };
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>"; };
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>"; };
@ -729,6 +731,7 @@
C4A47D8D2B7BBBEC00ADC637 /* StoreManager.swift */,
C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */,
C49EF0182BD694290077B5AA /* PurchaseListView.swift */,
C49EF0252BD80AE80077B5AA /* OffersHeaderView.swift */,
);
path = Subscription;
sourceTree = "<group>";
@ -1440,6 +1443,7 @@
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */,
C49EF0262BD80AE80077B5AA /* OffersHeaderView.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */,

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

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

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

@ -141,8 +141,14 @@ import LeStorage
func userFilteredPurchases() -> [StoreKit.Transaction] {
return self.purchasedTransactions.filter { transaction in
return Store.main.currentUserUUID() == transaction.appAccountToken
let userTransactions = self.purchasedTransactions.filter { Store.main.currentUserUUID() == $0.appAccountToken }
return userTransactions.filter { transaction in
if let expirationDate = transaction.expirationDate {
return expirationDate > Date()
} else {
return true
}
}
// return self.purchasedTransactions.filter { transaction in
@ -201,35 +207,12 @@ import LeStorage
}
var remainingTournaments: Int {
let subscriptionPayed = DataStore.shared.tournaments.filter { $0.payment?.isSubscription == true }
let unitlyPayed = DataStore.shared.tournaments.count - subscriptionPayed.count
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) ")
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 {

@ -8,9 +8,49 @@
import SwiftUI
struct OffersHeaderView: View {
let payment: Tournament.TournamentPayment? = .free
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 {

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

@ -103,7 +103,8 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
struct SubscriptionView: View {
@ObservedObject var model: SubscriptionModel = SubscriptionModel()
var showLackOfPlanMessage: Bool = false
@State var isRestoring: Bool = false
@State var showLoginView: Bool = false
@ -118,6 +119,11 @@ struct SubscriptionView: View {
} else {
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 {
Section {

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

Loading…
Cancel
Save