diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 91699b7..f907c1d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -151,6 +151,9 @@ FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; }; FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; }; FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; }; + FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; }; + FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; }; + FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; }; FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */; }; FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; }; FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; }; @@ -1038,6 +1041,7 @@ FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = ""; }; FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = ""; }; FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = ""; }; + FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentLinkManagerView.swift; sourceTree = ""; }; FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningByCourtView.swift; sourceTree = ""; }; FF2B51602C7E302C00FFF126 /* local.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = local.sqlite; sourceTree = ""; }; FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = ""; }; @@ -1890,6 +1894,7 @@ FF17CA562CC02FEA003C7323 /* CoachListView.swift */, FF7DCD382CC330260041110C /* TeamRestingView.swift */, FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */, + FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */, FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; @@ -2502,6 +2507,7 @@ FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, + FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, @@ -2779,6 +2785,7 @@ FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */, FF4CC0172C996C0600151637 /* PointView.swift in Sources */, + FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */, FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */, FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */, @@ -3034,6 +3041,7 @@ FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */, FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */, FF70FB962C90584900129CC2 /* PointView.swift in Sources */, + FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */, FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */, FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */, diff --git a/PadelClub/Utils/Network/PaymentService.swift b/PadelClub/Utils/Network/PaymentService.swift index 1eef4db..2be0200 100644 --- a/PadelClub/Utils/Network/PaymentService.swift +++ b/PadelClub/Utils/Network/PaymentService.swift @@ -13,8 +13,8 @@ class PaymentService { static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse { let service = try StoreCenter.main.service() let urlRequest = try service._baseRequest( - servicePath: "resend-payment-email/\(teamRegistrationId)/", - method: .post, + servicePath: "resend-payment-email/\(teamRegistrationId)/", + method: .post, requiresToken: true ) @@ -27,6 +27,42 @@ class PaymentService { return try JSON.decoder.decode(SimpleResponse.self, from: data) } + + static func getPaymentLink(teamRegistrationId: String) async throws -> PaymentLinkResponse { + let service = try StoreCenter.main.service() + let urlRequest = try service._baseRequest( + servicePath: "payment-link/\(teamRegistrationId)/", + method: .get, + requiresToken: true + ) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw PaymentError.requestFailed + } + +// // Debug: Print the raw JSON response +// if let jsonString = String(data: data, encoding: .utf8) { +// print("Raw JSON Response: \(jsonString)") +// } + + return try JSON.decoder.decode(PaymentLinkResponse.self, from: data) + } + +} + +struct PaymentLinkResponse: Codable { + let success: Bool + let paymentLink: String? + let message: String? + + enum CodingKeys: String, CodingKey { + case success + case paymentLink + case message + } } enum PaymentError: Error { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 811607f..b308869 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -31,6 +31,7 @@ struct EditingTeamView: View { @State private var registrationDateModified: Date @State private var uniqueRandomIndex: Int @State private var isDeleting: Bool = false + @State private var showPaymentLinkManager: Bool = false var messageSentFailed: Binding { Binding { @@ -155,6 +156,11 @@ struct EditingTeamView: View { if team.hasPaidOnline() == false { PaymentRequestButton(teamRegistration: team) +#if PRODTEST + Button("Récupérer le lien de paiement") { + showPaymentLinkManager = true + } +#endif } } @@ -409,6 +415,19 @@ struct EditingTeamView: View { } .tint(.master) } + .sheet(isPresented: $showPaymentLinkManager) { + NavigationStack { + PaymentLinkManagerView(teamRegistration: team) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Fermer") { + showPaymentLinkManager = false + } + } + } + } + } .fullScreenCover(item: $editedTeam) { editedTeam in NavigationStack { AddTeamView(tournament: tournament, editedTeam: editedTeam) diff --git a/PadelClub/Views/Team/PaymentLinkManagerView.swift b/PadelClub/Views/Team/PaymentLinkManagerView.swift new file mode 100644 index 0000000..573fa54 --- /dev/null +++ b/PadelClub/Views/Team/PaymentLinkManagerView.swift @@ -0,0 +1,268 @@ +// +// PaymentLinkManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 16/10/2025. +// + + +// +// PaymentLinkManagerView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 01/10/2025. +// + +import SwiftUI +import PadelClubData + +struct PaymentLinkManagerView: View { + let teamRegistration: TeamRegistration + @State private var isLoading = false + @State private var showAlert = false + @State private var alertMessage = "" + @State private var paymentLink: String? + @State private var showCopiedConfirmation = false + + var body: some View { + VStack(spacing: 20) { + // Header + header + + // Get Payment Link Button + getPaymentLinkButton + + // Payment Link Display and Actions + if let link = paymentLink { + paymentLinkSection(link: link) + } + + Spacer() + } + .padding() + .alert("Erreur", isPresented: $showAlert) { + Button("OK") { } + } message: { + Text(alertMessage) + } + } + + // MARK: - ViewBuilder Components + + @ViewBuilder + private var header: some View { + VStack(spacing: 8) { + Image(systemName: "creditcard.circle.fill") + .font(.system(size: 50)) + .foregroundColor(.blue) + + Text("Lien de paiement") + .font(.title2) + .fontWeight(.bold) + + Text("Obtenez un lien de paiement à partager avec l'équipe") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + + @ViewBuilder + private var getPaymentLinkButton: some View { + Button { + getPaymentLink() + } label: { + HStack { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .tint(.white) + } else { + Image(systemName: "link.circle") + } + Text(paymentLink == nil ? "Obtenir le lien de paiement" : "Régénérer le lien") + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(12) + } + .disabled(isLoading) + } + + @ViewBuilder + private func paymentLinkSection(link: String) -> some View { + VStack(spacing: 16) { + // Success message + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Lien copié dans le presse-papiers!") + .font(.subheadline) + .foregroundColor(.green) + } + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background(Color.green.opacity(0.1)) + .cornerRadius(8) + + // Link display + linkDisplayView(link: link) + + // Action buttons + actionButtons(link: link) + } + .transition(.move(edge: .top).combined(with: .opacity)) + .animation(.spring(response: 0.6, dampingFraction: 0.8), value: paymentLink) + } + + @ViewBuilder + private func linkDisplayView(link: String) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text("Lien de paiement:") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + + ScrollView(.horizontal, showsIndicators: false) { + Text(link) + .font(.system(.caption, design: .monospaced)) + .padding(12) + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + } + } + } + + @ViewBuilder + private func actionButtons(link: String) -> some View { + VStack(spacing: 12) { + // Copy button + copyButton(link: link) + + // Share button + shareButton(link: link) + + // Open in browser button + openInBrowserButton(link: link) + } + } + + @ViewBuilder + private func copyButton(link: String) -> some View { + Button { + UIPasteboard.general.string = link + showCopiedConfirmation = true + + // Haptic feedback + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + showCopiedConfirmation = false + } + } label: { + HStack { + Image(systemName: showCopiedConfirmation ? "checkmark.circle.fill" : "doc.on.doc.fill") + Text(showCopiedConfirmation ? "Copié !" : "Copier le lien") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding() + .background(showCopiedConfirmation ? Color.green : Color.blue.opacity(0.1)) + .foregroundColor(showCopiedConfirmation ? .white : .blue) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(showCopiedConfirmation ? Color.green : Color.blue, lineWidth: 1) + ) + } + .disabled(showCopiedConfirmation) + .animation(.easeInOut(duration: 0.2), value: showCopiedConfirmation) + } + + @ViewBuilder + private func shareButton(link: String) -> some View { + ShareLink(item: link) { + HStack { + Image(systemName: "square.and.arrow.up.fill") + Text("Partager le lien") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue.opacity(0.1)) + .foregroundColor(.blue) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.blue, lineWidth: 1) + ) + } + } + + @ViewBuilder + private func openInBrowserButton(link: String) -> some View { + Button { + if let url = URL(string: link) { + UIApplication.shared.open(url) + } + } label: { + HStack { + Image(systemName: "safari.fill") + Text("Ouvrir dans Safari") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue.opacity(0.1)) + .foregroundColor(.blue) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.blue, lineWidth: 1) + ) + } + } + + // MARK: - Private Methods + + private func getPaymentLink() { + isLoading = true + showCopiedConfirmation = false + + Task { + do { + let response = try await PaymentService.getPaymentLink( + teamRegistrationId: teamRegistration.id + ) + await MainActor.run { + isLoading = false + if response.success, let link = response.paymentLink { + paymentLink = link + // Automatically copy to clipboard + UIPasteboard.general.string = link + + // Haptic feedback + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } else { + alertMessage = response.message ?? "Impossible d'obtenir le lien de paiement" + showAlert = true + } + } + } catch { + await MainActor.run { + isLoading = false + alertMessage = "Erreur lors de la récupération du lien" + showAlert = true + } + } + } + } +} + +#Preview { + PaymentLinkManagerView(teamRegistration: TeamRegistration()) +} diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 96bc4e8..b8b8976 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -379,40 +379,6 @@ struct InscriptionManagerView: View { ToolbarItem(placement: .navigationBarTrailing) { Menu { - - #if PRODTEST - if tournament.enableOnlinePayment { - Button { - isLoading = true - Task { - do { - try await selectedSortedTeams.filter { team in - team.hasPaidOnline() == false && team.hasPaid() == false - }.concurrentForEach { team in - _ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id) - } - - await MainActor.run { - isLoading = false - alertMessage = "Relance effectuée avec succès" - showAlert = true - } - } catch { - Logger.error(error) - await MainActor.run { - isLoading = false - alertMessage = "Erreur lors de la requête" - showAlert = true - } - } - } - } label: { - Text("Requête de paiement") - } - .disabled(isLoading) - } - #endif - if tournament.inscriptionClosed() == false { Menu { _sortingTypePickerView() @@ -876,6 +842,31 @@ struct InscriptionManagerView: View { @ViewBuilder private func _informationView(for teams: [TeamRegistration]) -> some View { + #if PRODTEST + if tournament.enableOnlinePayment { + RowButtonView("Requête de paiement", role: .destructive) { + do { + try await teams.filter { team in + team.hasPaidOnline() == false && team.hasPaid() == false + }.concurrentForEach { team in + _ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id) + } + + await MainActor.run { + alertMessage = "Relance effectuée avec succès" + showAlert = true + } + } catch { + Logger.error(error) + await MainActor.run { + alertMessage = "Erreur lors de la requête" + showAlert = true + } + } + } + } + #endif + Section { HStack { // VStack(alignment: .leading, spacing: 0) {