diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 140f9a4..b938fc5 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1960,7 +1960,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -1985,7 +1985,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.3; + MARKETING_VERSION = 1.0.4; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; OTHER_SWIFT_FLAGS = "-Onone"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; @@ -2010,7 +2010,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -2033,7 +2033,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.3; + MARKETING_VERSION = 1.0.4; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50"; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 1fe097d..fba5442 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -14,7 +14,7 @@ extension String { } var trimmed: String { - trimmingCharacters(in: .whitespacesAndNewlines) + replaceCharactersFromSet(characterSet: .newlines).trimmingCharacters(in: .whitespacesAndNewlines) } func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { @@ -71,7 +71,7 @@ extension String { // Join the first letters together into a string let result = String(firstLetters) - return result + return String(result.prefix(10)) } } diff --git a/PadelClub/Utils/CloudConvert.swift b/PadelClub/Utils/CloudConvert.swift index 6a5a28f..ab1569a 100644 --- a/PadelClub/Utils/CloudConvert.swift +++ b/PadelClub/Utils/CloudConvert.swift @@ -29,196 +29,65 @@ class CloudConvert { static let manager = CloudConvert() func uploadFile(_ url: URL) async throws -> String { - let taskResponse = try await createJob(url) - let uploadResponse = try await uploadFile(taskResponse, url: url) - var fileReady = false - while fileReady == false { - try await Task.sleep(nanoseconds: 3_000_000_000) - let progressResponse = try await checkFile(id: uploadResponse.data.id) - if progressResponse.data.step == "finish" && progressResponse.data.stepPercent == 100 { - fileReady = true - print("progressResponse.data.minutes", progressResponse.data.minutes) - } - } - - let convertedFile = try await downloadConvertedFile(id: uploadResponse.data.id) - return convertedFile + return try await createJob(url) } - func createJob(_ url: URL) async throws -> TaskResponse { - guard let taskURL = URL(string: "https://api.convertio.co/convert") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert") + func createJob(_ url: URL) async throws -> String { + let apiPath = "https://\(URLs.activationHost.rawValue)/utils/xls-to-csv/" + guard let taskURL = URL(string: apiPath) else { + throw CloudConvertionError.urlNotFound(apiPath) } var request: URLRequest = URLRequest(url: taskURL) - let parameters = """ - {"apikey":"d97cf13ef6d163e5e386c381fc8d256f","input":"upload","file":"","filename":"","outputformat":"csv","options":""} - """ + request.httpMethod = "POST" + // Create the boundary string for multipart/form-data + let boundary = UUID().uuidString - let postData = parameters.data(using: .utf8) - request.httpMethod = "POST" - request.httpBody = postData + // Set the content type to multipart/form-data with the boundary + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - let task = try await URLSession.shared.data(for: request) - //print("tried: \(request.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - return try JSONDecoder().decode(TaskResponse.self, from: task.0) - } + // The file to upload + let fileName = url.lastPathComponent + let fileURL = url - func uploadFile(_ response: TaskResponse, url: URL) async throws -> UploadResponse { - guard let uploadTaskURL = URL(string: "https://api.convertio.co/convert/\(response.data.id)/\(url.encodedLastPathComponent)") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(response.data.id)/\(url.encodedLastPathComponent)") - } - var uploadRequest: URLRequest = URLRequest(url: uploadTaskURL) - uploadRequest.httpMethod = "PUT" - let uploadTask = try await URLSession.shared.upload(for: uploadRequest, fromFile: url) + // Construct the body of the request + var body = Data() - //print("tried: \(uploadRequest.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: uploadTask.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - return try JSONDecoder().decode(UploadResponse.self, from: uploadTask.0) - } - - func checkFile(id: String) async throws -> ProgressResponse { - guard let taskURL = URL(string: "https://api.convertio.co/convert/\(id)/status") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(id)/status") - } - var request: URLRequest = URLRequest(url: taskURL) - request.httpMethod = "GET" - let task = try await URLSession.shared.data(for: request) + // Start the body with the boundary and content-disposition for the file + body.append("--\(boundary)\r\n".data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) + body.append("Content-Type: application/vnd.ms-excel\r\n\r\n".data(using: .utf8)!) - //print("tried: \(request.url)") - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: task.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) + // Append the file data + if let fileData = try? Data(contentsOf: fileURL) { + body.append(fileData) } - - return try JSONDecoder().decode(ProgressResponse.self, from: task.0) - } - - func downloadConvertedFile(id: String) async throws -> String { -// try await Task.sleep(nanoseconds: 3_000_000_000) - - guard let downloadTaskURL = URL(string: "https://api.convertio.co/convert/\(id)/dl/base64") else { - throw CloudConvertionError.urlNotFound("https://api.convertio.co/convert/\(id)/dl/base64") - } - var downloadRequest: URLRequest = URLRequest(url: downloadTaskURL) - downloadRequest.httpMethod = "GET" - let downloadTask = try await URLSession.shared.data(for: downloadRequest) - if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: downloadTask.0) { - print("errorResponse.error", errorResponse.error) - throw CloudConvertionError.serviceError(errorResponse) - } - - //print("tried: \(downloadRequest.url)") - let dataResponse = try JSONDecoder().decode(DataResponse.self, from: downloadTask.0) - if let decodedData = Data(base64Encoded: dataResponse.data.content), let string = String(data: decodedData, encoding: .utf8) { - return string + + // End the body with the boundary + body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + // Set the body of the request + request.httpBody = body + + let (data, response) = try await URLSession.shared.data(for: request) + + // Check the response status code + if let httpResponse = response as? HTTPURLResponse { + print("Status code: \(httpResponse.statusCode)") } - throw CloudConvertionError.unknownError + // Convert the response data to a String + if let responseString = String(data: data, encoding: .utf8) { + return responseString + } else { + let error = ErrorResponse(code: 1, status: "Encodage", error: "Encodage des données de classement invalide") + throw CloudConvertionError.serviceError(error) + } } } -// MARK: - DataResponse -struct DataResponse: Decodable { - let code: Int - let status: String - let data: DataDownloadClass -} - -// MARK: - DataClass -struct DataDownloadClass: Decodable { - let id, encode, content: String -} - - // MARK: - ErrorResponse struct ErrorResponse: Decodable { let code: Int let status, error: String } - - -// MARK: - TaskResponse -struct TaskResponse: Decodable { - let code: Int - let status: String - let data: DataClass -} - -// MARK: - DataClass -struct DataClass: Decodable { - let id: String -} - -// MARK: - ProgressResponse -struct ProgressResponse: Decodable { - let code: Int - let status: String - let data: ProgressDataClass -} - -// MARK: - DataClass -struct ProgressDataClass: Decodable { - let id, step: String - let stepPercent: Int - let minutes: String - - enum CodingKeys: String, CodingKey { - case id, step - case stepPercent = "step_percent" - case minutes - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(String.self, forKey: .id) - step = try container.decode(String.self, forKey: .step) - minutes = try container.decode(String.self, forKey: .minutes) - if let value = try? container.decode(String.self, forKey: .stepPercent) { - print(value) - stepPercent = Int(value) ?? 0 - } else { - stepPercent = try container.decode(Int.self, forKey: .stepPercent) - } - } - -} - -// MARK: - Output -struct Output: Decodable { - let url: String - let size: String -} - -// MARK: - UploadResponse -struct UploadResponse: Decodable { - let code: Int - let status: String - let data: UploadDataClass -} - -// MARK: - DataClass -struct UploadDataClass: Decodable { - let id, file: String - let size: Int -} - -extension URL { - var encodedLastPathComponent: String { - if #available(iOS 17.0, *) { - lastPathComponent - } else { - lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? lastPathComponent - } - - } -} diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 41b0d99..0b6368c 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -14,7 +14,7 @@ enum PatchError: Error { enum Patch: String, CaseIterable { case alexisLeDu - case importDataFromDev + case importDataFromDevToProd var id: String { return "padelclub.app.patch.\(self.rawValue)" @@ -44,7 +44,7 @@ class Patcher { fileprivate static func _applyPatch(_ patch: Patch) throws { switch patch { case .alexisLeDu: self._patchAlexisLeDu() - case .importDataFromDev: try self._importDataFromDev() + case .importDataFromDevToProd: try self._importDataFromDev() } } diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index ad9c158..f83495b 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -9,9 +9,10 @@ import Foundation enum URLs: String, Identifiable { - case activationHost = "xlr.alwaysdata.net" // xlr.alwaysdata.net + case activationHost = "xlr.alwaysdata.net" case main = "https://xlr.alwaysdata.net/" case api = "https://xlr.alwaysdata.net/roads/" + case subscriptions = "https://apple.co/2Th4vqI" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" //case padelClub = "https://padelclub.app" @@ -24,13 +25,14 @@ enum URLs: String, Identifiable { case appDescription = "https://padelclub.app/download/" case instagram = "https://www.instagram.com/padelclub.app?igsh=bmticnV5YWhpMnBn" case appStore = "https://apps.apple.com/app/padel-club/id6484163558" + case eula = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" + case privacy = "https://padelclub.app/terms-of-use" var id: String { return self.rawValue } var url: URL { return URL(string: self.rawValue)! - } - + } } enum PageLink: String, Identifiable, CaseIterable { diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index ddb8123..56eaea8 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -68,7 +68,10 @@ class SearchViewModel: ObservableObject, Identifiable { var contentUnavailableMessage: String { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { - message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois. Dans ce pas, Padel Club ne pourra pas le trouver.") + message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") + if filterOption == .male { + message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.") + } } return message.joined(separator: "\n") } @@ -152,8 +155,9 @@ class SearchViewModel: ObservableObject, Identifiable { func orPredicate() -> NSPredicate? { var predicates : [NSPredicate] = [] - let canonicalVersionWithoutPunctuation = searchText.canonicalVersion - let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) + let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed + let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed switch tokens.first { case .none: if canonicalVersionWithoutPunctuation.isEmpty == false { diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index 29fe8f7..cb3b23b 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -66,6 +66,11 @@ struct ClubDetailView: View { .frame(maxWidth: .infinity) .focused($focusedField, equals: ._name) .submitLabel( displayContext == .addition ? .next : .done) + .onChange(of: club.name) { + if club.name.count > 50 { + club.name = String(club.name.prefix(50)) + } + } .onSubmit(of: .text) { if club.acronym.isEmpty { club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).acronym() @@ -90,7 +95,7 @@ struct ClubDetailView: View { .focused($focusedField, equals: ._acronym) .submitLabel(.done) .multilineTextAlignment(.trailing) - .onSubmit(of: .text) { + .onChange(of: club.acronym) { if club.acronym.count > 10 { club.acronym = String(club.acronym.prefix(10)) } else if club.acronym.count == 0 { diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 9677449..f0d9198 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -187,6 +187,14 @@ struct ToolboxView: View { } } + Section { + Link(destination: URLs.privacy.url) { + Text("Politique de confidentialité") + } + Link(destination: URLs.eula.url) { + Text("Contrat d'utilisation") + } + } } .overlay(alignment: .bottom) { if didResetApiCalls { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 56569a4..555a5ce 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -203,7 +203,10 @@ struct AddTeamView: View { } static private func _pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { - let text = pasteField.canonicalVersion + let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) + + // Remove all characters that are not in the allowedCharacterSet + let text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines) let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 } @@ -232,6 +235,7 @@ struct AddTeamView: View { let components = text.split(separator: " ").sorted() let pattern = components.joined(separator: ".*") + print(text, pattern) let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) orPredicates.append(canonicalFullNamePredicate) @@ -438,7 +442,7 @@ struct AddTeamView: View { ContentUnavailableView { Label("Aucun résultat", systemImage: "person.2.slash") } description: { - Text("Aucun joueur classé n'a été trouvé dans ce message.") + Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") } actions: { RowButtonView("Créer un joueur non classé") { presentPlayerCreation = true @@ -451,7 +455,8 @@ struct AddTeamView: View { } else { Section { - ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in + let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }) + ForEach(sortedPlayers) { player in ImportedPlayerView(player: player).tag(player.license!) } } header: { diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index 36c77cd..d0eec4e 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -141,14 +141,6 @@ import LeStorage var currentPlan: StoreItem? { return .monthlyUnlimited -// #if DEBUG -// return .monthlyUnlimited -// #else - if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) { - return plan - } - return nil -// #endif } func userFilteredPurchases() -> [StoreKit.Transaction] { diff --git a/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift b/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift index 6480449..a4e109f 100644 --- a/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift +++ b/PadelClub/Views/Tournament/Subscription/SubscriptionView.swift @@ -162,7 +162,6 @@ struct SubscriptionView: View { .buttonStyle(.borderedProminent) .tint(.orange) .listRowBackground(Color.clear) - } footer: { if product.item.isConsumable == false { SubscriptionFooterView() @@ -264,6 +263,10 @@ fileprivate struct ProductsSectionView: View { } } header: { Text("Sélectionnez une offre").foregroundStyle(Color(white: 0.8)) + } footer: { + let message = "Consulter notre [politique de confidentialité](\(URLs.privacy.rawValue)) et le [contrat d'utilisation](\(URLs.eula.rawValue)) de Padel Club." + Text(.init(message)) + .foregroundStyle(.white) } } diff --git a/PadelClub/Views/User/UserCreationView.swift b/PadelClub/Views/User/UserCreationView.swift index f994696..a20ebb4 100644 --- a/PadelClub/Views/User/UserCreationView.swift +++ b/PadelClub/Views/User/UserCreationView.swift @@ -142,8 +142,15 @@ struct UserCreationFormView: View { } Section { + Link(destination: URLs.privacy.url) { + Text("Politique de confidentialité") + } + Link(destination: URLs.eula.url) { + Text("Contrat d'utilisation") + } + Toggle(isOn: self.$dataCollectAuthorized) { - Text("J'autorise XLR Sport, éditeur de Padel Club, à sauvegarder les données ci-dessus. XLR Sport s'engage à ne pas partager ces données.").font(.footnote) + Text("J'accepte les conditions d'utilisations du service Padel Club proposé par XLR Sport").font(.footnote) } }