work in progress on subscriptions

multistore
Laurent 2 years ago
parent 13e09d2163
commit fd9583f2bb
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/Tournament.swift
  3. 4
      PadelClub/Manager/PadelRule.swift
  4. 19
      PadelClub/Manager/URLs.swift
  5. 41
      PadelClub/SyncedProducts.storekit
  6. 3
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  7. 95
      PadelClub/Views/Subscription/Guard.swift
  8. 4
      PadelClub/Views/Subscription/Purchase.swift
  9. 115
      PadelClub/Views/Subscription/PurchaseListView.swift
  10. 2
      PadelClub/Views/Subscription/StoreManager.swift
  11. 159
      PadelClub/Views/Subscription/SubscriptionView.swift
  12. 25
      PadelClub/Views/User/LoginView.swift

@ -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 = "<group>"; };
C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; 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>"; };
C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.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>"; };
@ -682,6 +686,7 @@
C4A47D8E2B7BBBEC00ADC637 /* Guard.swift */,
C4A47D8D2B7BBBEC00ADC637 /* StoreManager.swift */,
C4A47D8F2B7BBBEC00ADC637 /* StoreItem.swift */,
C49EF0182BD694290077B5AA /* PurchaseListView.swift */,
);
path = Subscription;
sourceTree = "<group>";
@ -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;
};

@ -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] = []

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

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

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

@ -14,6 +14,9 @@ struct UmpireView: View {
var body: some View {
NavigationStack {
List {
PurchaseListView()
NavigationLink {
MainUserView()
} label: {

@ -20,11 +20,11 @@ import LeStorage
var updateListenerTask: Task<Void, Error>? = nil
fileprivate var _purchases: StoredCollection<Purchase>
fileprivate(set) var purchases: StoredCollection<Purchase>
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)
}

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

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

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

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

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

Loading…
Cancel
Save