Refactor purchases and payments info

sync3
Laurent 6 months ago
parent 9547d13349
commit d910ca1646
  1. 14
      PadelClub.xcodeproj/project.pbxproj
  2. 8
      PadelClub/Data/DataStore.swift
  3. 4
      PadelClub/Extensions/Date+Extensions.swift
  4. 3
      PadelClub/Views/Planning/PlanningSettingsView.swift
  5. 19
      PadelClub/Views/Tournament/Subscription/Guard.swift
  6. 99
      PadelClub/Views/Tournament/Subscription/PaymentStatusView.swift
  7. 28
      PadelClub/Views/Tournament/Subscription/PurchaseListView.swift
  8. 2
      PadelClub/Views/Tournament/Subscription/SubscriptionInfoView.swift
  9. 3
      PadelClub/Views/Tournament/TournamentView.swift

@ -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 = "<group>"; };
C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleViewModifier.swift; sourceTree = "<group>"; };
C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingContainer+Extensions.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
C4D477982CB6704C0077713D /* SynchronizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationTests.swift; sourceTree = "<group>"; };
C4EC6F562BE92CAC000CEAB4 /* local.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = local.plist; sourceTree = "<group>"; };
C4EC6F582BE92D88000CEAB4 /* PListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PListReader.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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";
};

@ -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 })
}
}

@ -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))
}

@ -48,7 +48,8 @@ struct PlanningSettingsView: View {
var body: some View {
List {
if tournament.payment == nil {
SubscriptionInfoView()
ImageInfoView()
// SubscriptionInfoView()
}
Section {

@ -20,7 +20,7 @@ import LeStorage
var updateListenerTask: Task<Void, Never>? = 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 {

@ -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()
}
}

@ -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))
}
}

@ -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? {

@ -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() {

Loading…
Cancel
Save