From fd9583f2bb957b0b671f86c3eac2fde8d1406197 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Apr 2024 17:13:45 +0200 Subject: [PATCH] work in progress on subscriptions --- PadelClub.xcodeproj/project.pbxproj | 8 + PadelClub/Data/Tournament.swift | 2 +- PadelClub/Manager/PadelRule.swift | 4 +- PadelClub/Manager/URLs.swift | 19 +++ PadelClub/SyncedProducts.storekit | 41 ++++- .../Views/Navigation/Umpire/UmpireView.swift | 3 + PadelClub/Views/Subscription/Guard.swift | 95 +++++++---- PadelClub/Views/Subscription/Purchase.swift | 4 +- .../Views/Subscription/PurchaseListView.swift | 115 +++++++++++++ .../Views/Subscription/StoreManager.swift | 2 +- .../Views/Subscription/SubscriptionView.swift | 159 +++++++++++------- PadelClub/Views/User/LoginView.swift | 25 ++- 12 files changed, 367 insertions(+), 110 deletions(-) create mode 100644 PadelClub/Manager/URLs.swift create mode 100644 PadelClub/Views/Subscription/PurchaseListView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f613ca7..55b073d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44B79102BBDA63A00906534 /* Locale+Extensions.swift */; }; C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; }; 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 */; }; 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 */; }; @@ -286,6 +288,8 @@ C44B79102BBDA63A00906534 /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = ""; }; C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = ""; }; C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = ""; }; + C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = ""; }; + C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = ""; }; C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = ""; }; @@ -682,6 +686,7 @@ C4A47D8E2B7BBBEC00ADC637 /* Guard.swift */, C4A47D8D2B7BBBEC00ADC637 /* StoreManager.swift */, C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */, + C49EF0182BD694290077B5AA /* PurchaseListView.swift */, ); path = Subscription; sourceTree = ""; @@ -1037,6 +1042,7 @@ FFF8ACD02B9238A2008466FA /* Manager */ = { isa = PBXGroup; children = ( + C49EF01A2BD6A1E80077B5AA /* URLs.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */, @@ -1402,6 +1408,7 @@ FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, + C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, @@ -1453,6 +1460,7 @@ FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, + C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 67690a4..97820c6 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -43,7 +43,7 @@ class Tournament : ModelObject, Storable { var entryFee: Double? var maleUnrankedValue: Int? var femaleUnrankedValue: Int? - var payment: TournamentPayment = .free + var payment: TournamentPayment? = nil @ObservationIgnored var navigationPath: [Screen] = [] diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index cd55595..54d21e4 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -1463,7 +1463,7 @@ enum AnimationType: Int, CaseIterable, Hashable, Identifiable { case .playerAnimation: return "Par joueur" case .upAndDown: - return "Montante / Descandante" + return "Montante / Descendante" case .brawl: return "Brawl" } @@ -1476,7 +1476,7 @@ enum AnimationType: Int, CaseIterable, Hashable, Identifiable { case .upAndDown: return "Les gagnants montent sur le terrain d'à côté, les perdants descendent" case .brawl: - return "A chaque rotaiton, les gagnants de la rotation pécédente se jouent entre eux" + return "A chaque rotation, les gagnants de la rotation précédente se jouent entre eux" } } } diff --git a/PadelClub/Manager/URLs.swift b/PadelClub/Manager/URLs.swift new file mode 100644 index 0000000..3170d66 --- /dev/null +++ b/PadelClub/Manager/URLs.swift @@ -0,0 +1,19 @@ +// +// URLs.swift +// PadelClub +// +// Created by Laurent Morvillier on 22/04/2024. +// + +import Foundation + +enum URLs: String, Identifiable { + case subscriptions = "https://apple.co/2Th4vqI" + + var id: String { return self.rawValue } + + var url: URL { + return URL(string: self.rawValue)! + } + +} diff --git a/PadelClub/SyncedProducts.storekit b/PadelClub/SyncedProducts.storekit index 8b91825..35f21ef 100644 --- a/PadelClub/SyncedProducts.storekit +++ b/PadelClub/SyncedProducts.storekit @@ -22,9 +22,12 @@ ], "settings" : { "_applicationInternalID" : "6484163558", + "_compatibilityTimeRate" : { + "3" : 6 + }, "_developerTeamID" : "BQ3Y44M3Q6", "_failTransactionsEnabled" : false, - "_lastSynchronizedDate" : 734533081.06639695, + "_lastSynchronizedDate" : 735034894.72550702, "_locale" : "en_US", "_storefront" : "USA", "_storeKitErrors" : [ @@ -73,7 +76,8 @@ "enabled" : false, "name" : "Offer Code Redeem Sheet" } - ] + ], + "_timeRate" : 1001 }, "subscriptionGroups" : [ { @@ -83,6 +87,31 @@ ], "name" : "Main", "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "45.0", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "6498627737", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Créez jusqu'à 5 tournois chaque mois", + "displayName" : "Cinq tournois par mois", + "locale" : "fr" + } + ], + "productID" : "app.padelclub.tournament.subscription.five.per.month", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Monthly Five", + "subscriptionGroupID" : "21474782", + "type" : "RecurringSubscription" + }, { "adHocOffers" : [ @@ -93,18 +122,18 @@ "displayPrice" : "89.0", "familyShareable" : false, "groupNumber" : 1, - "internalID" : "6484163670", + "internalID" : "6498627536", "introductoryOffer" : null, "localizations" : [ { - "description" : "Créez autant de tournois que vous souhaitez", + "description" : "Créez des tournois sans limite ", "displayName" : "Abonnement illimité", "locale" : "fr" } ], - "productID" : "app.padelclub.unlimited", + "productID" : "app.padelclub.tournament.subscription.unlimited", "recurringSubscriptionPeriod" : "P1M", - "referenceName" : "Unlimited", + "referenceName" : "Monthly Unlimited", "subscriptionGroupID" : "21474782", "type" : "RecurringSubscription" } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index da95399..f667060 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -14,6 +14,9 @@ struct UmpireView: View { var body: some View { NavigationStack { List { + + PurchaseListView() + NavigationLink { MainUserView() } label: { diff --git a/PadelClub/Views/Subscription/Guard.swift b/PadelClub/Views/Subscription/Guard.swift index 9f20c66..c6c8401 100644 --- a/PadelClub/Views/Subscription/Guard.swift +++ b/PadelClub/Views/Subscription/Guard.swift @@ -20,11 +20,11 @@ import LeStorage var updateListenerTask: Task? = nil - fileprivate var _purchases: StoredCollection + fileprivate(set) var purchases: StoredCollection override init() { - self._purchases = Store.main.registerCollection(synchronized: true, inMemory: true) + self.purchases = Store.main.registerCollection(synchronized: true, inMemory: true, sendsUpdate: false) super.init() @@ -92,13 +92,22 @@ import LeStorage do { let purchase: Purchase = transaction.purchase() - try self._purchases.addOrUpdate(instance: purchase) +// let json = try? purchase.jsonString() ?? "nope" +// Logger.log("Add or update purchase = \(json) ") + try self.purchases.addOrUpdate(instance: purchase) } catch { Logger.error(error) } } else { // If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`. purchasedTransactions.remove(transaction) + if let existing = self.purchases.first(where: { $0.identifier == transaction.originalID }) { + do { + try self.purchases.delete(instance: existing) + } catch { + Logger.error(error) + } + } } self._updateBestPlan() } @@ -130,21 +139,26 @@ import LeStorage #endif } - fileprivate func _userFilteredPurchases() -> [StoreKit.Transaction] { + func userFilteredPurchases() -> [StoreKit.Transaction] { + return self.purchasedTransactions.filter { transaction in - return self._purchases.contains(where: { $0.identifier == transaction.id } ) + return Store.main.currentUserUUID() == transaction.appAccountToken } + +// return self.purchasedTransactions.filter { transaction in +// return self.purchases.contains(where: { $0.identifier == transaction.id } ) +// } } /// Update best plan by filtering Apple purchases with registered purchases by the user fileprivate func _updateBestPlan() { // Make sure the purchase has been done with the logged user - let privatePurchases = self._userFilteredPurchases() + let userPurchases = self.userFilteredPurchases() - if let unlimited = privatePurchases.first(where: { $0.productID == StoreItem.monthlyUnlimited.rawValue }) { + if let unlimited = userPurchases.first(where: { $0.productID == StoreItem.monthlyUnlimited.rawValue }) { self.currentBestPlan = unlimited - } else if let fivePerMonth = privatePurchases.first(where: { $0.productID == StoreItem.fivePerMonth.rawValue }) { + } else if let fivePerMonth = userPurchases.first(where: { $0.productID == StoreItem.fivePerMonth.rawValue }) { self.currentBestPlan = fivePerMonth } else { self.currentBestPlan = nil @@ -153,30 +167,10 @@ import LeStorage } fileprivate func _purchasedTournamentCount() -> Int { - let units = self._userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue } + let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue } return units.reduce(0) { $0 + $1.purchasedQuantity } } -// func canAddTournament() -> Bool { -// switch self.currentPlan { -// case .monthlyUnlimited: return true -// case .fivePerMonth: -// if let date = self.currentBestPlan?.originalPurchaseDate { -// let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > date } -// return tournaments.count < StoreItem.five -// } else { -// Logger.error(StoreManagerError.missingPlan) -// return false -// } -// case .unit: -// let tournamentCount = DataStore.shared.tournaments.count -// let tournamentCreditCount = self._purchasedTournamentCount() -// return tournamentCreditCount > tournamentCount -// case nil: -// return DataStore.shared.tournaments.count == 0 -// } -// } - func paymentForNewTournament() -> Tournament.TournamentPayment? { switch self.currentPlan { @@ -206,6 +200,44 @@ import LeStorage } + var remainingTournaments: Int { + let subscriptionPayed = DataStore.shared.tournaments.filter { $0.payment.isSubscription } + let unitlyPayed = DataStore.shared.tournaments.count - subscriptionPayed.count + let tournamentCreditCount = self._purchasedTournamentCount() + 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 { + var name: String + var item: StoreItem + var quantity: Int? + + var id: String { self.item.rawValue } } fileprivate extension StoreKit.Transaction { @@ -213,9 +245,10 @@ fileprivate extension StoreKit.Transaction { func purchase() -> Purchase { let userId = Store.main.currentUserUUID().uuidString return Purchase(user: userId, - identifier: self.id, + identifier: self.originalID, purchaseDate: self.purchaseDate, - productId: self.productID) + productId: self.productID, + quantity: self.purchasedQuantity) } diff --git a/PadelClub/Views/Subscription/Purchase.swift b/PadelClub/Views/Subscription/Purchase.swift index ad3f823..00f8a24 100644 --- a/PadelClub/Views/Subscription/Purchase.swift +++ b/PadelClub/Views/Subscription/Purchase.swift @@ -17,12 +17,14 @@ public class Purchase: ModelObject, Storable { public var identifier: UInt64 public var purchaseDate: Date public var productId: String + public var quantity: Int? - public init(user: String, identifier: UInt64, purchaseDate: Date, productId: String) { + public init(user: String, identifier: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil) { self.user = user self.identifier = identifier self.purchaseDate = purchaseDate self.productId = productId + self.quantity = quantity } } diff --git a/PadelClub/Views/Subscription/PurchaseListView.swift b/PadelClub/Views/Subscription/PurchaseListView.swift new file mode 100644 index 0000000..7396f5b --- /dev/null +++ b/PadelClub/Views/Subscription/PurchaseListView.swift @@ -0,0 +1,115 @@ +// +// PurchaseListView.swift +// PadelClub +// +// Created by Laurent Morvillier on 22/04/2024. +// + +import SwiftUI +import StoreKit +import LeStorage + +class PurchaseManager: ObservableObject { + + static let main: PurchaseManager = PurchaseManager() + + fileprivate var _products: [Product] = [] + fileprivate var _purchases: [Purchase] = [] + + @Published var purchaseRows: [PurchaseRow] = [] + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(_purchasesChanged(notification:)), name: NSNotification.Name.CollectionDidChange, object: Guard.main.purchases) + + let identifiers: [String] = StoreItem.allCases.map { $0.rawValue } + Task { + do { + self._products = try await Product.products(for: identifiers) + self._buildRows() + } catch { + Logger.error(error) + } + } + } + + @objc fileprivate func _purchasesChanged(notification: Notification) { + guard let collection = notification.object as? StoredCollection else { + return + } + self._purchases.removeAll() + self._purchases.append(contentsOf: collection) + self._buildRows() + } + + fileprivate func _buildRows() { + + DispatchQueue.main.async { + + 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)) + } + } + } + self.purchaseRows = rows + } + + } + +} + +struct PurchaseListView: View { + + @ObservedObject var manager = PurchaseManager() + + var body: some View { + + if self.manager.purchaseRows.count > 0 { + + Section { + ForEach(self.manager.purchaseRows) { purchaseRow in + + Link(destination: URLs.subscriptions.url) { + PurchaseView(purchaseRow: purchaseRow) + } + } + } header: { + Text("Vos achats") + } + + } + } +} + +struct PurchaseView: View { + + var purchaseRow: PurchaseRow + + var body: some View { + HStack { + Image(systemName: self.purchaseRow.item.systemImage) + .foregroundColor(.accentColor) + Text(self.purchaseRow.name) + Spacer() + if let quantity = purchaseRow.quantity { + let remaining = Guard.main.remainingTournaments + Text("\(remaining) / \(quantity.formatted())") + } + } + } +} + +#Preview { + PurchaseListView() +} diff --git a/PadelClub/Views/Subscription/StoreManager.swift b/PadelClub/Views/Subscription/StoreManager.swift index bf1699a..cf809c9 100644 --- a/PadelClub/Views/Subscription/StoreManager.swift +++ b/PadelClub/Views/Subscription/StoreManager.swift @@ -53,7 +53,7 @@ class StoreManager { let identifiers: [String] = StoreItem.allCases.map { $0.rawValue } Logger.log("Request products: \(identifiers)") - var products = try await Product.products(for: identifiers) + var products: [Product] = try await Product.products(for: identifiers) products = products.sorted { p1, p2 in return p2.price > p1.price } diff --git a/PadelClub/Views/Subscription/SubscriptionView.swift b/PadelClub/Views/Subscription/SubscriptionView.swift index 5dd1633..f52e550 100644 --- a/PadelClub/Views/Subscription/SubscriptionView.swift +++ b/PadelClub/Views/Subscription/SubscriptionView.swift @@ -46,6 +46,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate { @Published var quantity: Int = 1 { didSet { self._computePrice() + self.selectedProduct = self.products.first(where: { $0.id == StoreItem.unit.rawValue }) } } @Published var products: [Product] = [] @@ -53,12 +54,16 @@ class SubscriptionModel: ObservableObject, StoreDelegate { func load() { self.isLoading = true - self.storeManager = StoreManager(delegate: self) + if self.storeManager == nil { + self.storeManager = StoreManager(delegate: self) + } } func productsReceived(products: [Product]) { + self.isLoading = false self.products = products + Logger.log("products received = \(products.count)") } func errorDidOccur(error: Error) { @@ -100,79 +105,75 @@ struct SubscriptionView: View { @ObservedObject var model: SubscriptionModel = SubscriptionModel() @State var isRestoring: Bool = false + @State var showLoginView: Bool = false var body: some View { - VStack { - if self.model.products.count > 0 { - + Group { + if self.showLoginView { + LoginView { _ in + self.showLoginView = false + self._purchase() + } + } else { + Form { - - List { - ForEach(self.model.products) { product in - ProductView(product: product, - quantity: self.$model.quantity, selected: self.model.selectedProduct == product) - .onTapGesture { - self.model.selectedProduct = product + if self.model.products.count > 0 { + + Section { + + List { + ForEach(self.model.products) { product in + let isSelected = self.model.selectedProduct == product + ProductView(product: product, + quantity: self.$model.quantity, + selected: isSelected) + .onTapGesture { + self.model.selectedProduct = product + } + } } + } header: { + Text("Les offres") } - } - Section { - Button { - self._purchase() - } label: { - HStack { - Text("Purchase") - if let _ = self.model.selectedProduct { - Spacer() - Text(self.model.totalPrice) + + Section { + Button { + if Store.main.hasToken() { + self._purchase() + } else { + self.showLoginView = true + } + } label: { + HStack { + Text("Acheter") + if let _ = self.model.selectedProduct { + Spacer() + Text(self.model.totalPrice) + } } } + } footer: { + if self.model.selectedProduct?.item.isConsumable == false { + SubscriptionFooterView() + } } - } footer : { - if self.model.selectedProduct?.item.isConsumable == false { - - Text("Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte Apple.\n- L’abonnement est renouvelé automatiquement chaque mois, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l'offre gratuite, si souscrite, sera abandonnée lorsque l'utilisateur s'abonnera, dans les cas applicables.") - } - } - - - } - } else { - - VStack(alignment: .center) { - - if let error = self.model.error { - Text(error.localizedDescription) - } else { - Text("Aucun produit disponible") - } - - if self.model.isLoading { - ProgressView() - } else { - Button(action: { - self._load() - }, label: { - Image(systemName: "arrow.clockwise.circle.fill") - .font(.system(size: 64.0)) - }) } } - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - if self.isRestoring { - ProgressView() - } else { - Button("Restore") { - self._restore() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if self.isRestoring { + ProgressView() + } else { + Button("Restaurer") { + self._restore() + } + } } } } } - .navigationTitle("Subscriptions") + .navigationTitle("Abonnements") .onAppear { self._load() } @@ -221,14 +222,12 @@ struct ProductView: View { .foregroundColor(.accentColor) if self._isConsumable { StepperView(count: self.$quantity, minimum: 1).font(.callout) -// Stepper(value: self.$quantity) { -// Text("") -// } } } Spacer() if self.selected { - Image(systemName: "checkmark").foregroundColor(.accentColor) + Image(systemName: "checkmark") + .foregroundColor(.accentColor) } }.contentShape(.rect) } @@ -251,6 +250,40 @@ struct ProductView: View { } +struct SubscriptionNoProductView: View { + + @ObservedObject var model: SubscriptionModel + + var body: some View { + VStack(alignment: .center) { + + if let error = self.model.error { + Text(error.localizedDescription) + } else { + Text("Aucun produit disponible") + } + + if self.model.isLoading { + ProgressView() + } else { + Button(action: { + self.model.load() + }, label: { + Image(systemName: "arrow.clockwise.circle.fill") + .font(.system(size: 64.0)) + }) + } + } + + } +} + +struct SubscriptionFooterView: View { + var body: some View { + Text("Conditions d’utilisations concernant l’abonnement:\n- Le paiement sera facturé sur votre compte Apple.\n- L’abonnement est renouvelé automatiquement chaque mois, à moins d’avoir été désactivé au moins 24 heures avant la fin de la période de l’abonnement.\n- L’abonnement peut être géré par l’utilisateur et désactivé en allant dans les réglages de son compte après s’être abonné.\n- Le compte sera facturé pour le renouvellement de l'abonnement dans les 24 heures précédent la fin de la période d’abonnement.\n- Un abonnement en cours ne peut être annulé.\n- Toute partie inutilisée de l'offre gratuite, si souscrite, sera abandonnée lorsque l'utilisateur s'abonnera, dans les cas applicables.") + } +} + #Preview { NavigationStack { SubscriptionView() diff --git a/PadelClub/Views/User/LoginView.swift b/PadelClub/Views/User/LoginView.swift index 9b24fb1..580fc76 100644 --- a/PadelClub/Views/User/LoginView.swift +++ b/PadelClub/Views/User/LoginView.swift @@ -12,12 +12,13 @@ struct LoginView: View { @EnvironmentObject var dataStore: DataStore - @State var username: String = "razmig" - @State var password: String = "StaxKikoo12" + @State var username: String = "laurent" + @State var password: String = "staxstax" + @State var isLoading: Bool = false @State var showEmailPopup: Bool = false - @State var error: Error? = nil + @State var errorText: String? = nil var showEmailValidationMessage: Bool = false @@ -43,9 +44,15 @@ struct LoginView: View { Button(action: { self._login() }, label: { - Text("Login") + if self.isLoading { + ProgressView() + } else { + Text("Login").frame(maxWidth: .infinity) + } }) - .frame(maxWidth: .infinity) + if let error = self.errorText { + Text(error).font(.callout).foregroundStyle(.red) + } } if !self.showEmailValidationMessage { @@ -74,6 +81,7 @@ struct LoginView: View { } fileprivate func _login() { + self.errorText = nil // reset error Task { do { let service = try Store.main.service() @@ -83,6 +91,13 @@ struct LoginView: View { self.dataStore.setUser(user) self.handler(user) } catch { + switch error { + case ServiceError.responseError(let reason): + self.errorText = reason + default: + self.errorText = error.localizedDescription + } + Logger.error(error) } }