Various fixes

multistore
Laurent 1 year ago
parent 64b6ad40ec
commit 246541a9ec
  1. 8
      PadelClub/Views/Calling/CallView.swift
  2. 99
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  3. 278
      PadelClub/Views/Match/MatchDetailView.swift
  4. 10
      PadelClub/Views/Subscription/Guard.swift
  5. 63
      PadelClub/Views/Subscription/SubscriptionView.swift
  6. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  7. 38
      PadelClub/Views/User/LoginView.swift

@ -172,10 +172,12 @@ struct CallView: View {
}) })
.sheet(isPresented: self.$showUserCreationView, content: { .sheet(isPresented: self.$showUserCreationView, content: {
NavigationStack { NavigationStack {
LoginView { _ in LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false self.showUserCreationView = false
self._summon(byMessage: self.summonParamByMessage, self._payTournamentAndExecute {
reSummon: self.summonParamByMessage) self._summon(byMessage: self.summonParamByMessage,
reSummon: self.summonParamByMessage)
}
} }
} }
}) })

@ -6,8 +6,10 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct MenuWarningView: View { struct MenuWarningView: View {
let tournament: Tournament
let teams: [TeamRegistration] let teams: [TeamRegistration]
var date: Date? var date: Date?
var message: String? var message: String?
@ -16,6 +18,11 @@ struct MenuWarningView: View {
@Binding var contactType: ContactType? @Binding var contactType: ContactType?
@State var showSubscriptionView: Bool = false
@State var showUserCreationView: Bool = false
@State var savedContactType: ContactType? = nil
private func _getUmpireMail() -> [String]? { private func _getUmpireMail() -> [String]? {
if let umpireMail { if let umpireMail {
return [umpireMail] return [umpireMail]
@ -23,7 +30,40 @@ struct MenuWarningView: View {
return nil return nil
} }
// TODO: Guard var body: some View {
Menu {
if let team = teams.first, teams.count == 1 {
_teamActionView(team)
} else {
Menu("Tout le monde") {
let players = teams.flatMap({ $0.players() })
_actionView(players: players, privateMode: true)
}
Divider()
ForEach(teams) { team in
_teamView(team)
}
}
} label: {
Text("Prévenir")
.underline()
}
.sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
}
})
.sheet(isPresented: self.$showUserCreationView, content: {
NavigationStack {
LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false
self._tryToContact()
}
}
})
}
@ViewBuilder @ViewBuilder
private func _actionView(players: [PlayerRegistration], privateMode: Bool = false) -> some View { private func _actionView(players: [PlayerRegistration], privateMode: Bool = false) -> some View {
if players.count == 1, let player = players.first, let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { if players.count == 1, let player = players.first, let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {
@ -47,32 +87,21 @@ struct MenuWarningView: View {
} }
Button("Message") { Button("Message") {
contactType = .message(date: date, recipients: players.compactMap({ $0.phoneNumber }), body: message, tournamentBuild: nil) self._contactByMessage(players: players, privateMode: privateMode)
} }
Button("Mail") { Button("Mail") {
contactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : players.compactMap({ $0.email }), bccRecipients: privateMode ? players.compactMap({ $0.email }) : nil, body: message, subject: subject, tournamentBuild: nil) self._contactByMail(players: players, privateMode: privateMode)
} }
} }
var body: some View { fileprivate func _contactByMessage(players: [PlayerRegistration], privateMode: Bool) {
self.savedContactType = .message(date: date, recipients: players.compactMap({ $0.phoneNumber }), body: message, tournamentBuild: nil)
Menu { self._tryToContact()
if let team = teams.first, teams.count == 1 { }
_teamActionView(team)
} else { fileprivate func _contactByMail(players: [PlayerRegistration], privateMode: Bool) {
Menu("Tout le monde") { self.savedContactType = .mail(date: date, recipients: privateMode ? _getUmpireMail() : players.compactMap({ $0.email }), bccRecipients: privateMode ? players.compactMap({ $0.email }) : nil, body: message, subject: subject, tournamentBuild: nil)
let players = teams.flatMap({ $0.players() }) self._tryToContact()
_actionView(players: players, privateMode: true)
}
Divider()
ForEach(teams) { team in
_teamView(team)
}
}
} label: {
Text("Prévenir")
.underline()
}
} }
func _playerView(_ player: PlayerRegistration) -> some View { func _playerView(_ player: PlayerRegistration) -> some View {
@ -103,6 +132,32 @@ struct MenuWarningView: View {
_playerView(player) _playerView(player)
} }
} }
fileprivate func _tryToContact() {
self._verifyUser {
self._payTournamentAndExecute {
self.contactType = self.savedContactType
}
}
}
fileprivate func _verifyUser(_ handler: () -> ()) {
if Store.main.userId != nil {
handler()
} else {
self.showUserCreationView = true
}
}
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do {
try tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
}
} }
//#Preview { //#Preview {

@ -27,8 +27,11 @@ struct MatchDetailView: View {
@State private var showDetails: Bool = false @State private var showDetails: Bool = false
@State private var contactType: ContactType? = nil @State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false // @State private var showSubscriptionView: Bool = false
@State var showSubscriptionView: Bool = false
@State var showUserCreationView: Bool = false
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
sentError != nil sentError != nil
@ -65,52 +68,6 @@ struct MatchDetailView: View {
} }
} }
var quickLookHeader: some View {
Section {
HStack {
Menu {
Button("Non défini") {
match.removeCourt()
save()
}
if let tournament = match.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Button(tournament.courtName(atIndex: courtIndex)) {
match.setCourt(courtIndex)
save()
}
}
}
} label: {
VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let courtName = match.courtName() {
Text(courtName)
.foregroundStyle(Color.master)
.underline()
} else {
Text("Choisir")
.foregroundStyle(Color.master)
.underline()
}
}
}
Spacer()
MatchDateView(match: match, showPrefix: true)
}
.font(.title)
.buttonStyle(.plain)
} footer: {
// if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData {
// HStack {
// WeatherView(weatherData: weatherData)
// }
// }
// }
}
}
var body: some View { var body: some View {
List { List {
if match.hasWalkoutTeam() == false { if match.hasWalkoutTeam() == false {
@ -130,7 +87,7 @@ struct MatchDetailView: View {
showDetails = true showDetails = true
} }
Spacer() Spacer()
MenuWarningView(teams: match.teams(), message: match.matchWarningMessage(), umpireMail: dataStore.user.email, subject: match.matchWarningSubject(), contactType: $contactType) MenuWarningView(tournament: self.match.currentTournament()!, teams: match.teams(), message: match.matchWarningMessage(), umpireMail: dataStore.user.email, subject: match.matchWarningSubject(), contactType: $contactType)
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
} }
@ -139,23 +96,14 @@ struct MatchDetailView: View {
if match.isReady() { if match.isReady() {
Section { Section {
RowButtonView("Saisir les résultats", systemImage: "list.clipboard") { RowButtonView("Saisir les résultats", systemImage: "list.clipboard") {
do { self._editScores()
if let tournament = self.match.currentTournament() {
try tournament.payIfNecessary()
scoreType = .edition
} else {
self.showSubscriptionView = true
}
} catch {
self.showSubscriptionView = true
}
} }
} }
} }
let players = match.teams().flatMap { $0.players() } let players = self.match.teams().flatMap { $0.players() }
let unpaid = players.filter({ $0.hasPaid() == false }) let unpaid = players.filter({ $0.hasPaid() == false })
if unpaid.isEmpty == false { if unpaid.isEmpty == false {
Section { Section {
DisclosureGroup { DisclosureGroup {
@ -186,6 +134,14 @@ struct MatchDetailView: View {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true) SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
} }
}) })
.sheet(isPresented: self.$showUserCreationView, content: {
NavigationStack {
LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false
self._editScores()
}
}
})
.sheet(item: $scoreType, onDismiss: { .sheet(item: $scoreType, onDismiss: {
if match.hasEnded() { if match.hasEnded() {
dismiss() dismiss()
@ -194,38 +150,38 @@ struct MatchDetailView: View {
let matchDescriptor = MatchDescriptor(match: match) let matchDescriptor = MatchDescriptor(match: match)
EditScoreView(matchDescriptor: matchDescriptor) EditScoreView(matchDescriptor: matchDescriptor)
.tint(.master) .tint(.master)
// switch scoreType { // switch scoreType {
// case .edition: // case .edition:
// let matchDescriptor = MatchDescriptor(match: match) // let matchDescriptor = MatchDescriptor(match: match)
// EditScoreView(matchDescriptor: matchDescriptor) // EditScoreView(matchDescriptor: matchDescriptor)
// case .live: // case .live:
// if let score = match.score { // if let score = match.score {
// if score.sets.isEmpty { // if score.sets.isEmpty {
// SplashView(score: score) // SplashView(score: score)
// } else { // } else {
// NewLiveScoringView(score: score) // NewLiveScoringView(score: score)
// } // }
// } // }
// case .prepare: // case .prepare:
// if match.freeMatchTeams.isEmpty == false { // if match.freeMatchTeams.isEmpty == false {
// EditFreeMatchView(match: match) // EditFreeMatchView(match: match)
// } else { // } else {
// PrepareMatchView(match: match) // PrepareMatchView(match: match)
// } // }
// case .stat: // case .stat:
// if let score = match.score { // if let score = match.score {
// MatchStatView() // MatchStatView()
// .environmentObject(score) // .environmentObject(score)
// } // }
// case .health: // case .health:
// HealthKitView(match: match) // HealthKitView(match: match)
// .presentationDetents([.medium]) // .presentationDetents([.medium])
// case .feeling: // case .feeling:
// if let feedbackData = match.feedbackData { // if let feedbackData = match.feedbackData {
// FeedbackView(feedbackData: feedbackData) // FeedbackView(feedbackData: feedbackData)
// } // }
// } // }
} }
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
@ -239,47 +195,35 @@ struct MatchDetailView: View {
Group { Group {
switch contactType { switch contactType {
case .message(_, let recipients, let body, _): case .message(_, let recipients, let body, _):
if Guard.main.paymentForNewTournament() != nil { MessageComposeView(recipients: recipients, body: body) { result in
MessageComposeView(recipients: recipients, body: body) { result in switch result {
switch result { case .cancelled:
case .cancelled: break
break case .failed:
case .failed: self.sentError = .messageFailed
self.sentError = .messageFailed case .sent:
case .sent: if networkMonitor.connected == false {
if networkMonitor.connected == false { self.sentError = .messageNotSent
self.sentError = .messageNotSent
}
@unknown default:
break
} }
} @unknown default:
} else { break
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
} }
} }
case .mail(_, let recipients, let bccRecipients, let body, let subject, _): case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
if Guard.main.paymentForNewTournament() != nil { MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in switch result {
switch result { case .cancelled, .saved:
case .cancelled, .saved: self.contactType = nil
self.contactType = nil case .failed:
case .failed: self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
self.sentError = .mailFailed self.sentError = .mailNotSent
case .sent:
if networkMonitor.connected == false {
self.contactType = nil
self.sentError = .mailNotSent
}
@unknown default:
break
} }
} @unknown default:
} else { break
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
} }
} }
} }
@ -305,7 +249,7 @@ struct MatchDetailView: View {
} label: { } label: {
Text("Supprimer l'horaire") Text("Supprimer l'horaire")
} }
Button(role: .destructive) { Button(role: .destructive) {
match.resetScores() match.resetScores()
match.resetMatch() match.resetMatch()
@ -314,7 +258,7 @@ struct MatchDetailView: View {
} label: { } label: {
Text("Supprimer les scores") Text("Supprimer les scores")
} }
} label: { } label: {
LabelOptions() LabelOptions()
} }
@ -326,6 +270,52 @@ struct MatchDetailView: View {
} }
var quickLookHeader: some View {
Section {
HStack {
Menu {
Button("Non défini") {
match.removeCourt()
save()
}
if let tournament = match.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Button(tournament.courtName(atIndex: courtIndex)) {
match.setCourt(courtIndex)
save()
}
}
}
} label: {
VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let courtName = match.courtName() {
Text(courtName)
.foregroundStyle(Color.master)
.underline()
} else {
Text("Choisir")
.foregroundStyle(Color.master)
.underline()
}
}
}
Spacer()
MatchDateView(match: match, showPrefix: true)
}
.font(.title)
.buttonStyle(.plain)
} footer: {
// if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData {
// HStack {
// WeatherView(weatherData: weatherData)
// }
// }
// }
}
}
enum ScoreType: Int, Identifiable, Hashable { enum ScoreType: Int, Identifiable, Hashable {
var id: Int { var id: Int {
self.rawValue self.rawValue
@ -442,6 +432,34 @@ struct MatchDetailView: View {
} }
} }
fileprivate func _editScores() {
self._verifyUser {
self._payTournamentAndExecute {
self.scoreType = .edition
}
}
}
fileprivate func _verifyUser(_ handler: () -> ()) {
if Store.main.userId != nil {
handler()
} else {
self.showUserCreationView = true
}
}
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
guard let tournament = self.match.currentTournament() else { fatalError("missing tournament") }
do {
try tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
}
private func save() { private func save() {
do { do {
try dataStore.matches.addOrUpdate(instance: match) try dataStore.matches.addOrUpdate(instance: match)

@ -140,14 +140,14 @@ import LeStorage
} }
var currentPlan: StoreItem? { var currentPlan: StoreItem? {
return .monthlyUnlimited // return .monthlyUnlimited
// #if DEBUG // #if DEBUG
// return .monthlyUnlimited // return .monthlyUnlimited
// #else // #else
// if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) { if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) {
// return plan return plan
// } }
// return nil return nil
// #endif // #endif
} }

@ -46,7 +46,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
@Published var quantity: Int = 1 @Published var quantity: Int = 1
@Published var products: [Product] = [] @Published var products: [Product] = []
@Published var totalPrice: String = "" @Published var totalPrice: String = ""
init() { init() {
self.load() self.load()
} }
@ -72,7 +72,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
self.isLoading = false self.isLoading = false
self.error = error self.error = error
} }
func purchase() async throws -> Bool { func purchase() async throws -> Bool {
Logger.log("start purchase...") Logger.log("start purchase...")
guard let product: Product = self.selectedProduct, let storeManager = self.storeManager else { guard let product: Product = self.selectedProduct, let storeManager = self.storeManager else {
@ -110,7 +110,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
struct SubscriptionView: View { struct SubscriptionView: View {
@ObservedObject var model: SubscriptionModel = SubscriptionModel() @ObservedObject var model: SubscriptionModel = SubscriptionModel()
@Binding var isPresented: Bool @Binding var isPresented: Bool
var showLackOfPlanMessage: Bool = false var showLackOfPlanMessage: Bool = false
@ -118,7 +118,7 @@ struct SubscriptionView: View {
@State var showLoginView: Bool = false @State var showLoginView: Bool = false
@State var isPurchasing: Bool = false @State var isPurchasing: Bool = false
@State var showSuccessfulPurchaseView: Bool = false @State var showSuccessfulPurchaseView: Bool = false
init(isPresented: Binding<Bool>, showLackOfPlanMessage: Bool = false) { init(isPresented: Binding<Bool>, showLackOfPlanMessage: Bool = false) {
self._isPresented = isPresented self._isPresented = isPresented
self.showLackOfPlanMessage = showLackOfPlanMessage self.showLackOfPlanMessage = showLackOfPlanMessage
@ -126,7 +126,13 @@ struct SubscriptionView: View {
var body: some View { var body: some View {
VStack { VStack(alignment: .leading) {
Text("Abonnements")
.font(.system(size: 36.0))
.fontWeight(.bold)
.padding(.horizontal)
.foregroundStyle(.white)
if self.showLoginView { if self.showLoginView {
LoginView { _ in LoginView { _ in
self.showLoginView = false self.showLoginView = false
@ -156,7 +162,7 @@ struct SubscriptionView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(.orange) .tint(.orange)
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
} footer: { } footer: {
if product.item.isConsumable == false { if product.item.isConsumable == false {
SubscriptionFooterView() SubscriptionFooterView()
@ -172,18 +178,27 @@ struct SubscriptionView: View {
} }
.listStyle(.grouped) .listStyle(.grouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(.logoBackground)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Restaurer") {
self._restore()
}.isLoading(self.isRestoring)
}
}
} }
} }
.preferredColorScheme(.dark) .background(.logoBackground)
.navigationTitle("Abonnements") .toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Restaurer") {
self._restore()
}.isLoading(self.isRestoring)
}
}
// .toolbar {
// ToolbarItem(placement: .principal) {
// VStack(spacing: -4.0) {
// Text("Abonnements").font(.headline).foregroundStyle(.white)
// }
// }
//
// }.navigationBarTitleDisplayMode(.inline)
// .preferredColorScheme(.dark)
// .navigationTitle("Abonnements")
} }
@ -198,7 +213,7 @@ struct SubscriptionView: View {
fileprivate func _purchase() { fileprivate func _purchase() {
self.isPurchasing = true self.isPurchasing = true
Task { Task {
do { do {
let success = try await self.model.purchase() let success = try await self.model.purchase()
@ -248,9 +263,9 @@ fileprivate struct ProductsSectionView: View {
} }
} }
} header: { } header: {
Text("Sélectionnez une offre") Text("Sélectionnez une offre").foregroundStyle(Color(white: 0.8))
} }
} }
} }
@ -295,8 +310,8 @@ struct ProductView: View {
@ObservedObject var model: SubscriptionModel @ObservedObject var model: SubscriptionModel
// @Binding var quantity: Int // @Binding var quantity: Int
// var selected: Bool // var selected: Bool
var body: some View { var body: some View {
HStack { HStack {
@ -325,7 +340,7 @@ struct ProductView: View {
.tint(Color.master) .tint(Color.master)
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
} }
fileprivate var _item: StoreItem? { fileprivate var _item: StoreItem? {
return StoreItem(rawValue: self.product.id) return StoreItem(rawValue: self.product.id)
} }
@ -368,7 +383,7 @@ fileprivate struct SubscriptionNoProductView: View {
}) })
} }
} }
} }
} }
@ -384,7 +399,7 @@ fileprivate struct SubscriptionDetailView: View {
var body: some View { var body: some View {
HStack { HStack {
Image(systemName: "exclamationmark.bubble.fill") Image(systemName: "exclamationmark.bubble.fill")
//.foregroundStyle(Color.accentColor) //.foregroundStyle(Color.accentColor)
.font(.title) .font(.title)
Text("Vous êtes arrivé à limite de votre offre actuelle. Voici ce que nous proposons pour poursuivre votre tournoi:") Text("Vous êtes arrivé à limite de votre offre actuelle. Voici ce que nous proposons pour poursuivre votre tournoi:")
.fontWeight(.semibold) .fontWeight(.semibold)

@ -1118,7 +1118,7 @@ struct InscriptionManagerView: View {
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu { Menu {
Section { Section {
MenuWarningView(teams: [team], contactType: $contactType) MenuWarningView(tournament: team.tournamentObject()!, teams: [team], contactType: $contactType)
//Divider() //Divider()
Button("Copier") { Button("Copier") {
let pasteboard = UIPasteboard.general let pasteboard = UIPasteboard.general

@ -8,6 +8,32 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
enum LoginReason {
case loginRequiredForFeature
var title: String {
switch self {
case .loginRequiredForFeature:
return "Compte requis"
}
}
var systemImage: String {
switch self {
case .loginRequiredForFeature:
return "person.crop.circle.fill"
}
}
var description: String {
switch self {
case .loginRequiredForFeature:
return "Pour accéder à cette fonctionnalité, veuillez-vous connecter ou créer un compte"
}
}
}
typealias Credentials = (username: String, password: String) typealias Credentials = (username: String, password: String)
struct LoginView: View { struct LoginView: View {
@ -27,6 +53,8 @@ struct LoginView: View {
@State var errorText: String? = nil @State var errorText: String? = nil
@FocusState var focusedField: UserLogin? @FocusState var focusedField: UserLogin?
var reason: LoginReason? = nil
var showEmailValidationMessage: Bool { var showEmailValidationMessage: Bool {
credentials != nil credentials != nil
} }
@ -64,6 +92,16 @@ struct LoginView: View {
} }
} }
if let reason {
Section {
ContentUnavailableView {
Label(reason.title, systemImage: reason.systemImage)
} description: {
Text(reason.description)
}
}
}
Section { Section {
TextField("Nom d'utilisateur", text: self.$username) TextField("Nom d'utilisateur", text: self.$username)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)

Loading…
Cancel
Save