add payment link api

main
Razmig Sarkissian 3 weeks ago
parent bd03321cc0
commit b5d5cd4aeb
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 40
      PadelClub/Utils/Network/PaymentService.swift
  3. 19
      PadelClub/Views/Team/EditingTeamView.swift
  4. 268
      PadelClub/Views/Team/PaymentLinkManagerView.swift
  5. 59
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -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 = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentLinkManagerView.swift; sourceTree = "<group>"; };
FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningByCourtView.swift; sourceTree = "<group>"; };
FF2B51602C7E302C00FFF126 /* local.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = local.sqlite; sourceTree = "<group>"; };
FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = "<group>"; };
@ -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 */,

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

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

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

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

Loading…
Cancel
Save