Merge branch 'main' of https://gitea.staxriver.com/staxriver/PadelClub
commit
f6cf835ebf
@ -0,0 +1,8 @@ |
|||||||
|
## Padel Club |
||||||
|
|
||||||
|
This is the main directory of a Swift app that helps padel tournament organizers. |
||||||
|
The project is structured around three projects linked in the PadelClub.xcworkspace: |
||||||
|
- PadelClub: this one, which mostly contains the UI for the project |
||||||
|
- PadelClubData: the business logic for the app |
||||||
|
- LeStorage: a local storage with a synchronization layer |
||||||
|
|
||||||
@ -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()) |
||||||
|
} |
||||||
Loading…
Reference in new issue