Laurent 1 year ago
commit 32447ef056
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Extensions/String+Extensions.swift
  3. 203
      PadelClub/Utils/CloudConvert.swift
  4. 4
      PadelClub/Utils/Patcher.swift
  5. 6
      PadelClub/Utils/URLs.swift
  6. 10
      PadelClub/ViewModel/SearchViewModel.swift
  7. 7
      PadelClub/Views/Club/ClubDetailView.swift
  8. 8
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  9. 11
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  10. 8
      PadelClub/Views/Tournament/Subscription/Guard.swift
  11. 5
      PadelClub/Views/Tournament/Subscription/SubscriptionView.swift
  12. 9
      PadelClub/Views/User/UserCreationView.swift

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

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

@ -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":""}
"""
let postData = parameters.data(using: .utf8)
request.httpMethod = "POST"
request.httpBody = postData
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)
}
// Create the boundary string for multipart/form-data
let boundary = UUID().uuidString
return try JSONDecoder().decode(TaskResponse.self, from: task.0)
}
// Set the content type to multipart/form-data with the boundary
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
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)
//print("tried: \(uploadRequest.url)")
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: uploadTask.0) {
print("errorResponse.error", errorResponse.error)
throw CloudConvertionError.serviceError(errorResponse)
}
// The file to upload
let fileName = url.lastPathComponent
let fileURL = url
return try JSONDecoder().decode(UploadResponse.self, from: uploadTask.0)
}
// Construct the body of the request
var body = Data()
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)
}
// End the body with the boundary
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
func downloadConvertedFile(id: String) async throws -> String {
// try await Task.sleep(nanoseconds: 3_000_000_000)
// Set the body of the request
request.httpBody = body
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)
}
let (data, response) = try await URLSession.shared.data(for: request)
//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
// 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
}
}
}

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

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

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

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

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

@ -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: {

@ -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] {

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

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

Loading…
Cancel
Save