From d910ca164646da2ec0ceb95323c241e8f5db9e8e Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 29 Apr 2025 17:46:21 +0200 Subject: [PATCH] Refactor purchases and payments info --- PadelClub.xcodeproj/project.pbxproj | 14 +++ PadelClub/Data/DataStore.swift | 8 ++ PadelClub/Extensions/Date+Extensions.swift | 4 + .../Views/Planning/PlanningSettingsView.swift | 3 +- .../Views/Tournament/Subscription/Guard.swift | 19 ++-- .../Subscription/PaymentStatusView.swift | 99 +++++++++++++++++++ .../Subscription/PurchaseListView.swift | 28 ++++-- .../Subscription/SubscriptionInfoView.swift | 2 +- .../Views/Tournament/TournamentView.swift | 3 +- 9 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7abe733..51f606e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -161,6 +161,10 @@ C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */; }; C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; + C4D05D472DC10AE5009B053C /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4D05D462DC10AE5009B053C /* WebKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + C4D05D492DC10CBE009B053C /* PaymentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D05D482DC10CBE009B053C /* PaymentStatusView.swift */; }; + C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D05D482DC10CBE009B053C /* PaymentStatusView.swift */; }; + C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D05D482DC10CBE009B053C /* PaymentStatusView.swift */; }; C4D477992CB6704C0077713D /* SynchronizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D477982CB6704C0077713D /* SynchronizationTests.swift */; }; C4EC6F572BE92CAC000CEAB4 /* local.plist in Resources */ = {isa = PBXBuildFile; fileRef = C4EC6F562BE92CAC000CEAB4 /* local.plist */; }; C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC6F582BE92D88000CEAB4 /* PListReader.swift */; }; @@ -1144,6 +1148,8 @@ C4B3A1542C2581DA0078EAA8 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = ""; }; C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleViewModifier.swift; sourceTree = ""; }; C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingContainer+Extensions.swift"; sourceTree = ""; }; + C4D05D462DC10AE5009B053C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + C4D05D482DC10CBE009B053C /* PaymentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStatusView.swift; sourceTree = ""; }; C4D477982CB6704C0077713D /* SynchronizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationTests.swift; sourceTree = ""; }; C4EC6F562BE92CAC000CEAB4 /* local.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = local.plist; sourceTree = ""; }; C4EC6F582BE92D88000CEAB4 /* PListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PListReader.swift; sourceTree = ""; }; @@ -1458,6 +1464,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C4D05D472DC10AE5009B053C /* WebKit.framework in Frameworks */, FFCFBFFE2BBBE86600B82851 /* Algorithms in Frameworks */, FF92660D2C241CE0002361A4 /* Zip in Frameworks */, C49EF0392BDFF4600077B5AA /* LeStorage.framework in Frameworks */, @@ -1583,6 +1590,7 @@ C425D4592B6D255B002A7B48 /* Frameworks */ = { isa = PBXGroup; children = ( + C4D05D462DC10AE5009B053C /* WebKit.framework */, C49EF0372BDFF3000077B5AA /* LeStorage.framework */, ); name = Frameworks; @@ -1706,6 +1714,7 @@ C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */, C49EF0182BD694290077B5AA /* PurchaseListView.swift */, C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */, + C4D05D482DC10CBE009B053C /* PaymentStatusView.swift */, ); path = Subscription; sourceTree = ""; @@ -2691,6 +2700,7 @@ C488C8342CC7E4240082001F /* BaseRound.swift in Sources */, C488C8352CC7E4240082001F /* BaseMatchScheduler.swift in Sources */, C488C8372CC7E4240082001F /* BasePlayerRegistration.swift in Sources */, + C4D05D492DC10CBE009B053C /* PaymentStatusView.swift in Sources */, C488C8382CC7E4240082001F /* BaseTeamScore.swift in Sources */, FFE8B5BB2DA9896800BDE966 /* RefundService.swift in Sources */, C488C8392CC7E4240082001F /* BaseTournament.swift in Sources */, @@ -3152,6 +3162,7 @@ FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF4CBFF02C996C0600151637 /* URLs.swift in Sources */, + C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, @@ -3452,6 +3463,7 @@ FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */, FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */, FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */, + C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */, FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, @@ -3721,6 +3733,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -3766,6 +3779,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index f496b75..655733c 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -391,5 +391,13 @@ class DataStore: ObservableObject { _cachedEndMatches = runningMatches.sorted(by: \.endDate!, order: .descending) return _cachedEndMatches! } + + func subscriptionUnitlyPayedTournaments(after date: Date) -> Int { + return DataStore.shared.tournaments.count(where: { tournament in + tournament.creationDate > date && + tournament.payment == .subscriptionUnit && + tournament.isCanceled == false && + tournament.isDeleted == false }) + } } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 291f6ba..73fb079 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -56,6 +56,10 @@ extension Date { formatted(.dateTime.weekday().day(.twoDigits).month().year()) } + var dateFormatted: String { + formatted(.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits)) + } + var monthYearFormatted: String { formatted(.dateTime.month(.wide).year(.defaultDigits)) } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 8b1ab94..a600e66 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -48,7 +48,8 @@ struct PlanningSettingsView: View { var body: some View { List { if tournament.payment == nil { - SubscriptionInfoView() + ImageInfoView() +// SubscriptionInfoView() } Section { diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index 77106fa..9cbdf79 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -20,7 +20,7 @@ import LeStorage var updateListenerTask: Task? = nil - fileprivate let _freeTournaments: Int = 3 + let freeTournaments: Int = 3 override init() { @@ -34,6 +34,7 @@ import LeStorage } catch { Logger.error(error) } + Logger.log("plan = \(String(describing: currentBestPurchase?.productId))") } NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) @@ -263,8 +264,8 @@ import LeStorage return TournamentPayment.unlimited case .fivePerMonth: if let purchaseDate = self.currentBestPurchase?.purchaseDate { - let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false } - if tournaments.count < StoreItem.five { + let count = DataStore.shared.subscriptionUnitlyPayedTournaments(after: purchaseDate) + if count < StoreItem.five { return TournamentPayment.subscriptionUnit } } @@ -277,7 +278,7 @@ import LeStorage fileprivate func _paymentWithoutSubscription() -> TournamentPayment? { let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count - if freelyPayed < self._freeTournaments { + if freelyPayed < self.freeTournaments { return TournamentPayment.free } let tournamentCreditCount: Int = self._purchasedTournamentCount() @@ -291,12 +292,14 @@ import LeStorage var remainingTournaments: Int { let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.unit }.count let tournamentCreditCount = self._purchasedTournamentCount() -// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count - -// Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ") return tournamentCreditCount - unitlyPayed } + var remainingFreeTournaments: Int { + let freelyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.free }.count + return self.freeTournaments - freelyPayed + } + func disconnect() { let purchases = DataStore.shared.purchases purchases.reset() @@ -309,6 +312,8 @@ struct PurchaseRow: Identifiable { var name: String var item: StoreItem var quantity: Int? + var expirationDate: Date? + var remainingCount: Int? = nil } fileprivate extension StoreKit.Transaction { diff --git a/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift new file mode 100644 index 0000000..9a59492 --- /dev/null +++ b/PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift @@ -0,0 +1,99 @@ +// +// PaymentStatusView.swift +// PadelClub +// +// Created by Laurent Morvillier on 29/04/2025. +// + +import SwiftUI +import TipKit + +struct ImageInfoView: View { + + @State var systemImage: String = "" + @State var text: String = "" + @State var textColor: Color = .black + @State var backgroundColor: Color = .blue.opacity(0.2) + + @State var textOnTap: String? = nil + + @State var showPopover: Bool = false + + var tip: (any Tip)? = nil + + var body: some View { + Group { + if #available(iOS 18.4, *) { + HStack { + Image(systemName: self.systemImage) + .font(.title) + .foregroundStyle(.white) + Text(self.text) + .foregroundStyle(self.textColor) + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + }.popoverTip(self.tip) + } else { + HStack { + Image(systemName: self.systemImage) + .font(.title) + .foregroundStyle(.white) + Text(self.text) + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + }.onTapGesture { + if self.tip != nil { + self.showPopover = true + } + } + } + } + .alert("Message", isPresented: self.$showPopover, actions: { }, message: { + if let tip { + tip.title + } else { + Text("") + } + }) + .frame(maxWidth: .infinity) + .padding() + .background(self.backgroundColor) + + .listRowInsets(EdgeInsets()) // Remove default insets + } +} + +struct PaymentStatusView: View { + + @State var payment: TournamentPayment? = .free + + var body: some View { + + Group { + switch self.payment { + case .free: + let remaining = Guard.main.remainingFreeTournaments + let end = remaining > 1 ? "s" : "" + let text = "Tournoi offert (\(remaining) restant\(end))" + ImageInfoView(systemImage: "gift.fill", text: text, tip: FreeTournamentTip()) +// TipView(FreeTournamentTip()).tipStyle(tint: nil, background: .blue.opacity(0.2)) + case nil: + ImageInfoView(systemImage: "exclamationmark.bubble.fill", text: "Veuillez souscrire à une offre pour convoquer ou entrer un résultat", textColor: .white, backgroundColor: .logoRed, tip: NoPaymentTip()) +// TipView(NoPaymentTip()).tipStyle(tint: nil, background: .logoRed) + default: + EmptyView() + } + }.onAppear { +// self.payment = nil + self.payment = Guard.main.paymentForNewTournament() + } + + } + +} + +#Preview { + List { + PaymentStatusView() + } +} diff --git a/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift b/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift index e704e93..cd7bd68 100644 --- a/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift +++ b/PadelClub/Views/Tournament/Subscription/PurchaseListView.swift @@ -55,7 +55,12 @@ class PurchaseManager: ObservableObject { if let userPurchase = userPurchases.first(where: { $0.productID == subscription.rawValue }), let product = self._products.first(where: { $0.id == subscription.rawValue }) { - rows.append(PurchaseRow(id: userPurchase.originalID, name: product.displayName, item: subscription)) + var remainingCount: Int? = nil + if subscription == .fivePerMonth { + remainingCount = StoreItem.five - DataStore.shared.subscriptionUnitlyPayedTournaments(after: userPurchase.purchaseDate) + } + + rows.append(PurchaseRow(id: userPurchase.originalID, name: product.displayName, item: subscription, expirationDate: userPurchase.expirationDate, remainingCount: remainingCount)) } } @@ -106,16 +111,27 @@ struct PurchaseView: View { HStack { Image(systemName: self.purchaseRow.item.systemImage) .foregroundColor(.accentColor).font(.title2) - Text(self.purchaseRow.name) + VStack(alignment: .leading) { + Text(self.purchaseRow.name) + if let expirationDate = purchaseRow.expirationDate { + Text("Expire le \(expirationDate.dateFormatted)").font(.footnote) + .foregroundStyle(.gray) + } + } Spacer() if let _ = purchaseRow.quantity { let remaining = Guard.main.remainingTournaments - Text("\(remaining)") + Text(remaining.formatted()) + } + if let count = purchaseRow.remainingCount { + Text(count.formatted()) } } } } -//#Preview { -// PurchaseListView() -//} +#Preview { + List { + PurchaseView(purchaseRow: PurchaseRow(id: 0, name: "test", item: .fivePerMonth, expirationDate: Date(), remainingCount: 4)) + } +} diff --git a/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift b/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift index 2354c0e..d501299 100644 --- a/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift @@ -33,7 +33,7 @@ struct SubscriptionInfoView: View { struct FreeTournamentTip: Tip { var title: Text { - return Text("Nous vous offrons vos 3 premiers tournois ! 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 !") + return Text("Nous vous offrons vos 3 premiers tournois ! 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 !\n\n Votre tournoi est décompté lorsque vous convoquez ou que vous rentrez un résultat.") } var image: Image? { diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 74fa700..56a4878 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -51,7 +51,8 @@ struct TournamentView: View { VStack(spacing: 0.0) { List { if tournament.state() != .finished && tournament.payment == nil { - SubscriptionInfoView() + PaymentStatusView() +// SubscriptionInfoView() } switch tournament.state() {