Laurent 1 year ago
commit 414f74af65
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 11
      PadelClub/Data/AppSettings.swift
  3. 2
      PadelClub/Data/Club.swift
  4. 13
      PadelClub/Info.plist
  5. 5
      PadelClub/PadelClub.entitlements
  6. 92
      PadelClub/PadelClubApp.swift
  7. 1
      PadelClub/Utils/URLs.swift
  8. 2
      PadelClub/ViewModel/NavigationViewModel.swift
  9. 56
      PadelClub/Views/Club/ClubDetailView.swift
  10. 9
      PadelClub/Views/Club/Shared/ClubCourtSetupView.swift
  11. 4
      PadelClub/Views/Navigation/MainView.swift
  12. 2
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  13. 16
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  14. 59
      PadelClub/Views/User/LoginView.swift
  15. 102
      PadelClub/Views/User/UserCreationView.swift

@ -499,6 +499,7 @@
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; };
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; };
FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; };
FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = "<group>"; };
FF9267F72BCE78C70080F940 /* CashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierView.swift; sourceTree = "<group>"; };
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierDetailView.swift; sourceTree = "<group>"; };
FF9267FB2BCE84870080F940 /* PlayerPayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPayView.swift; sourceTree = "<group>"; };
@ -640,6 +641,7 @@
C425D3FF2B6D249D002A7B48 /* PadelClub */ = {
isa = PBXGroup;
children = (
FF92660F2C255E4A002361A4 /* PadelClub.entitlements */,
FFE2D2D72C216D4800D0C7BE /* GoogleService-Info.plist */,
FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */,
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */,
@ -1904,8 +1906,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -1944,8 +1947,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -13,12 +13,23 @@ import SwiftUI
class AppSettings: MicroStorable {
var lastDataSource: String? = nil
var didCreateAccount: Bool = false
var didRegisterAccount: Bool = false
required init() {
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource)
didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false
didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false
}
enum CodingKeys: String, CodingKey {
case _lastDataSource = "lastDataSource"
case _didCreateAccount = "didCreateAccount"
case _didRegisterAccount = "didRegisterAccount"
}
}

@ -216,8 +216,6 @@ extension Club {
}
func hasBeenCreated(by creatorId: String?) -> Bool {
guard let creatorId else { return false }
guard let creator else { return false }
return creatorId == creator
}

@ -18,6 +18,19 @@
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>app.padelclub</string>
<key>CFBundleURLSchemes</key>
<array>
<string>padelclub</string>
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIFileSharingEnabled</key>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

@ -15,7 +15,33 @@ struct PadelClubApp: App {
let persistenceController = PersistenceController.shared
@State private var navigationViewModel = NavigationViewModel()
@StateObject var networkMonitor: NetworkMonitor = NetworkMonitor()
@StateObject var dataStore = DataStore.shared
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@State private var registrationError: RegistrationError? = nil
var presentError: Binding<Bool> {
Binding {
registrationError != nil
} set: { value in
if value == false {
registrationError = nil
}
}
}
enum RegistrationError: LocalizedError {
case badActivationLink
case activationFailed
var errorDescription: String? {
switch self {
case .badActivationLink:
return "Le lien d'activation n'a pas fonctionné"
case .activationFailed:
return "L'activation de votre compte n'a pas fonctionné"
}
}
}
static var appVersion: String {
let dictionary = Bundle.main.infoDictionary!
@ -27,7 +53,19 @@ struct PadelClubApp: App {
var body: some Scene {
WindowGroup {
MainView()
.alert(isPresented: presentError, error: registrationError) {
Button("Contactez-nous") {
_openMail()
}
Button("Annuler", role: .cancel) {
registrationError = nil
}
}
.onOpenURL { url in
_handleIncomingURL(url)
}
.environmentObject(networkMonitor)
.environmentObject(dataStore)
.environment(navigationViewModel)
.accentColor(.master)
.onAppear {
@ -47,11 +85,65 @@ struct PadelClubApp: App {
}
}
private func _handleIncomingURL(_ url: URL) {
// Parse the URL
let pathComponents = url.pathComponents
if url.scheme == "padelclub" && pathComponents.count > 3 && pathComponents[1] == "activate" {
let uidb64 = pathComponents[2]
let token = pathComponents[3]
_activateUser(uidb64: uidb64, token: token)
print(uidb64, token)
} else {
// Handle invalid URL case
print("Handle invalid URL case")
registrationError = .badActivationLink
}
}
private func _activateUser(uidb64: String, token: String) {
guard let url = URL(string: "https://\(URLs.activationHost.rawValue)/activate/\(uidb64)/\(token)/") else {
registrationError = .activationFailed
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("activation error")
print(error)
registrationError = .activationFailed
return
}
DispatchQueue.main.async {
dataStore.appSettings.didRegisterAccount = true
dataStore.appSettingsStorage.write()
if navigationViewModel.selectedTab != .umpire {
navigationViewModel.selectedTab = .umpire
}
if navigationViewModel.umpirePath.isEmpty {
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
} else if navigationViewModel.umpirePath.last! != .login {
navigationViewModel.umpirePath.removeAll()
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
}
}
}.resume()
}
fileprivate func _onAppear() {
let docURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
Logger.log("doc dir = \(docURL.absoluteString)")
UserDefaults.standard.set(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
}
private func _openMail(emailTo: String = "support@padelclub.app", subject: String = "Support Padel Club") {
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {

@ -8,6 +8,7 @@
import Foundation
enum URLs: String, Identifiable {
case activationHost = "xlr.alwaysdata.net"
case subscriptions = "https://apple.co/2Th4vqI"
case main = "https://xlr.alwaysdata.net/"
case api = "https://xlr.alwaysdata.net/roads/"

@ -11,7 +11,7 @@ import SwiftUI
class NavigationViewModel {
var path = NavigationPath()
var toolboxPath = NavigationPath()
var umpirePath = NavigationPath()
var umpirePath: [UmpireView.UmpireScreen] = []
var ongoingPath = NavigationPath()
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity

@ -18,6 +18,7 @@ struct ClubDetailView: View {
@State private var selectedCourt: Court?
@Bindable var club: Club
@State private var clubDeleted: Bool = false
@State private var confirmDeletion: Bool = false
var displayContext: DisplayContext
var selection: ((Club) -> ())? = nil
@ -69,6 +70,8 @@ struct ClubDetailView: View {
}
}
.disabled(displayContext == .lockedForEditing)
LabeledContent {
if acronymMode == .automatic || displayContext == .lockedForEditing {
Text(club.acronym)
@ -134,7 +137,6 @@ struct ClubDetailView: View {
if displayContext == .addition {
focusedField = ._zipCode
}
club.city = city
}
} label: {
Text("Ville")
@ -151,9 +153,6 @@ struct ClubDetailView: View {
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._zipCode)
.submitLabel( displayContext == .addition ? .next : .done)
.onSubmit {
club.zipCode = zipCode
}
} label: {
Text("Code Postal")
}
@ -179,7 +178,7 @@ struct ClubDetailView: View {
Text("Accéder au club sur Tenup")
}
}
if let padelClubLink {
if let padelClubLink, club.creator != nil {
Link(destination: padelClubLink) {
Text("Accéder au club sur Padel Club")
}
@ -217,15 +216,7 @@ struct ClubDetailView: View {
if displayContext == .edition {
Section {
RowButtonView("Supprimer ce club", role: .destructive) {
do {
clubDeleted = true
dataStore.user.clubs.removeAll(where: { $0 == club.id })
self.dataStore.saveUser()
try dataStore.clubs.deleteById(club.id)
dismiss()
} catch {
Logger.error(error)
}
_deleteClub()
}
}
}
@ -241,6 +232,12 @@ struct ClubDetailView: View {
.navigationDestination(item: $selectedCourt) { court in
CourtView(court: court)
}
.onChange(of: zipCode) {
club.zipCode = zipCode
}
.onChange(of: city) {
club.city = city
}
.onDisappear {
if displayContext == .edition && clubDeleted == false {
do {
@ -257,6 +254,37 @@ struct ClubDetailView: View {
}
}
}
.toolbar {
if displayContext == .edition {
ToolbarItem(placement: .topBarTrailing) {
Button(role: .destructive) {
confirmDeletion = true
} label: {
LabelDelete()
}
}
}
}
.confirmationDialog("Êtes-vous sûr de vouloir supprimer ce club ?", isPresented: $confirmDeletion, titleVisibility: .visible) {
Button("Oui, je suis sûr") {
_deleteClub()
}
Button("Annuler", role: .cancel) {
}
}
}
private func _deleteClub() {
do {
clubDeleted = true
dataStore.user.clubs.removeAll(where: { $0 == club.id })
self.dataStore.saveUser()
try dataStore.clubs.deleteById(club.id)
dismiss()
} catch {
Logger.error(error)
}
}
}

@ -51,6 +51,12 @@ struct ClubCourtSetupView: View {
ForEach((0..<club.courtCount), id: \.self) { courtIndex in
_courtView(atIndex: courtIndex, tournamentClub: club)
}
} header: {
Text("Nom des terrains")
} footer: {
if displayContext == .lockedForEditing && hideLockForEditingMessage == false {
Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed)
}
}
}
@ -58,7 +64,6 @@ struct ClubCourtSetupView: View {
private func _courtView(atIndex index: Int, tournamentClub: Club) -> some View {
let court = tournamentClub.customizedCourts.first(where: { $0.index == index })
LabeledContent {
if displayContext == .edition {
FooterButtonView("personnaliser") {
if let court {
selectedCourt = court
@ -72,7 +77,7 @@ struct ClubCourtSetupView: View {
selectedCourt = newCourt
}
}
}
.disabled(displayContext == .lockedForEditing)
} label: {
if let court {
Text(court.courtTitle())

@ -9,7 +9,7 @@ import SwiftUI
import LeStorage
struct MainView: View {
@StateObject var dataStore = DataStore.shared
@EnvironmentObject var dataStore: DataStore
@AppStorage("importingFiles") var importingFiles: Bool = false
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@ -54,7 +54,7 @@ struct MainView: View {
}
var badgeText: Text? {
return (Store.main.userName() != nil && _isConnected() == false) ? Text("!").font(.headline) : nil
return (dataStore.appSettings.didCreateAccount && _isConnected() == false) ? Text("!").font(.headline) : nil
}
var body: some View {

@ -123,7 +123,7 @@ struct ToolboxView: View {
}
Section {
Link("Accéder au guide de la compétition", destination: URLs.padelRules.url)
Link("Accéder au guide de la compétition de la FFT", destination: URLs.padelRules.url)
}
}
.navigationTitle(TabDestination.toolbox.title)

@ -32,6 +32,10 @@ struct UmpireView: View {
return URL.importDateFormatter.date(from: lastDataSource)
}
enum UmpireScreen {
case login
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.umpirePath) {
@ -56,9 +60,7 @@ struct UmpireView: View {
AccountRowView(userName: dataStore.user.username)
}
} else {
NavigationLink {
LoginView {_ in }
} label: {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
@ -210,6 +212,12 @@ struct UmpireView: View {
}
}
}
.navigationDestination(for: UmpireScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
}
}
@ -232,7 +240,7 @@ struct AccountRowView: View {
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte Padel Club", systemImage: "person.fill")
Label("Mon compte", systemImage: "person.fill")
if dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}

@ -8,10 +8,12 @@
import SwiftUI
import LeStorage
typealias Credentials = (username: String, password: String)
struct LoginView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State var username: String = ""
@State var password: String = ""
@ -19,10 +21,20 @@ struct LoginView: View {
@State var showEmailPopup: Bool = false
@State var showUserCreationForm: Bool = false
@State var showEmailValidationMessage: Bool = false
@State var showSubscriptionView: Bool = false
@State var credentials: Credentials?
@State var errorText: String? = nil
@FocusState var focusedField: UserLogin?
var showEmailValidationMessage: Bool {
credentials != nil
}
enum UserLogin {
case username
case password
}
var loginFailed: Binding<Bool> {
Binding {
@ -40,15 +52,36 @@ struct LoginView: View {
Form {
if self.showEmailValidationMessage || (dataStore.appSettings.didCreateAccount == true && dataStore.appSettings.didRegisterAccount == false) {
Section {
ContentUnavailableView {
Label("Vérifiez vos emails.", systemImage: "envelope.badge")
} description: {
Text("Vous pouvez maintenant ouvrir votre boîte mail pour valider votre compte. Vous pourrez ensuite vous connecter ici. N'oubliez pas de vérifiez vos spams !")
} actions: {
SupportButtonView(contentIsUnavailable: true)
}
}
}
Section {
TextField("Nom d'utilisateur", text: self.$username)
.autocorrectionDisabled()
.autocorrectionDisabled(true)
.keyboardType(.asciiCapable)
.textContentType(.init(rawValue: ""))
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit(of: .text) {
focusedField = .password
}
SecureField("Mot de passe", text: self.$password)
} header: {
if self.showEmailValidationMessage {
Text("Vous pouvez maintenant ouvrir votre boîte mail pour valider votre compte. Vous pourrez ensuite vous connecter ici.")
.textContentType(.init(rawValue: ""))
.submitLabel(.send)
.onSubmit(of: .text) {
Task {
await self._login()
}
}
}
@ -68,8 +101,13 @@ struct LoginView: View {
}, label: {
Text("Créer un compte")
})
.sheet(isPresented: self.$showUserCreationForm) {
UserCreationFormView(isPresented: self.$showUserCreationForm, showEmailValidationMessage: self.$showEmailValidationMessage)
.sheet(isPresented: self.$showUserCreationForm, onDismiss: {
if let credentials {
self.username = credentials.username
self.password = credentials.password
}
}) {
UserCreationFormView(isPresented: self.$showUserCreationForm, credentials: self.$credentials)
}
@ -124,7 +162,12 @@ struct LoginView: View {
password: self.password)
self.dataStore.user = user
self.isLoading = false
await MainActor.run {
dataStore.appSettings.didRegisterAccount = true
dataStore.appSettingsStorage.write()
}
self.handler(user)
navigation.umpirePath.removeAll()
} catch {
self.isLoading = false
self.errorText = ErrorUtils.message(error: error)

@ -10,21 +10,11 @@ import LeStorage
struct UserCreationFormView: View {
@FocusState private var focusedField: Field?
fileprivate enum Field: Int, Hashable {
case username
case password1
case password2
case email
case firstName
case lastName
case phone
}
@EnvironmentObject var networkMonitor: NetworkMonitor
@EnvironmentObject var dataStore: DataStore
@Binding var isPresented: Bool
@Binding var showEmailValidationMessage: Bool
@Binding var credentials: Credentials?
@State var username: String = ""
@State var email: String = ""
@ -51,6 +41,18 @@ struct UserCreationFormView: View {
@State var isLoading = false
@FocusState var focusedField: UserCreationFormField?
enum UserCreationFormField {
case username
case email
case password
case confirmPassword
case firstName
case lastName
case phoneNumber
}
var body: some View {
NavigationStack {
@ -58,46 +60,74 @@ struct UserCreationFormView: View {
Section {
TextField("Nom d'utilisateur", text: self.$username)
.autocorrectionDisabled()
.textContentType(.init(rawValue: ""))
.keyboardType(.asciiCapable)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.focused($focusedField, equals: Field.username)
.onSubmit { self.focusedField = Field.email }
.onSubmit(of: .text) {
focusedField = .email
}
TextField("Email", text: self.$email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .email)
.submitLabel(.next)
.focused($focusedField, equals: Field.email)
.onSubmit { self.focusedField = Field.password1 }
.onSubmit(of: .text) {
focusedField = .password
}
}
Section {
SecureField("Mot de passe", text: self.$password1)
.focused($focusedField, equals: .password)
.textContentType(.init(rawValue: ""))
.keyboardType(.asciiCapable)
.autocorrectionDisabled(true)
.submitLabel(.next)
.focused($focusedField, equals: Field.password1)
.onSubmit { self.focusedField = Field.password2 }
.onSubmit(of: .text) {
focusedField = .confirmPassword
}
SecureField("Confirmez le mot de passe", text: self.$password2)
.focused($focusedField, equals: .confirmPassword)
.textContentType(.init(rawValue: ""))
.keyboardType(.asciiCapable)
.autocorrectionDisabled(true)
.submitLabel(.next)
.focused($focusedField, equals: Field.password2)
.onSubmit { self.focusedField = Field.firstName }
.onSubmit(of: .text) {
focusedField = .firstName
}
}
Section {
TextField("Prénom", text: self.$firstName)
.focused($focusedField, equals: .firstName)
.textContentType(.init(rawValue: ""))
.keyboardType(.asciiCapable)
.autocorrectionDisabled(true)
.submitLabel(.next)
.autocorrectionDisabled()
.focused($focusedField, equals: Field.firstName)
.onSubmit { self.focusedField = Field.lastName }
.onSubmit(of: .text) {
focusedField = .lastName
}
TextField("Nom", text: self.$lastName)
.focused($focusedField, equals: .lastName)
.textContentType(.init(rawValue: ""))
.keyboardType(.asciiCapable)
.autocorrectionDisabled(true)
.submitLabel(.next)
.autocorrectionDisabled()
.focused($focusedField, equals: Field.lastName)
.onSubmit { self.focusedField = Field.phone }
.onSubmit(of: .text) {
focusedField = .phoneNumber
}
TextField("Téléphone", text: self.$phone)
.submitLabel(.next)
.autocorrectionDisabled()
.focused($focusedField, equals: Field.phone)
// .onSubmit { self.focusNextField($focusedField) }
.focused($focusedField, equals: .phoneNumber)
.keyboardType(.default)
.textContentType(.telephoneNumber)
.autocorrectionDisabled(true)
.submitLabel(.done)
LabeledContent {
Picker(selection: $selectedCountryIndex) {
@ -125,9 +155,13 @@ struct UserCreationFormView: View {
}
.disabled(!self.dataCollectAuthorized)
}
}.navigationTitle("Créez votre compte")
}
.navigationTitle("Créez votre compte")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
.onAppear {
self.focusedField = .username
self._selectCountry()
}
.alert(self.alertMessage, isPresented: self.$showAlertView, actions: {
@ -197,8 +231,10 @@ struct UserCreationFormView: View {
DispatchQueue.main.async {
self.isLoading = false
self.showEmailValidationMessage = true
self.credentials = Credentials(username: username, password: password1)
self.isPresented = false
dataStore.appSettings.didCreateAccount = true
dataStore.appSettingsStorage.write()
}
} catch {

Loading…
Cancel
Save