fix issue with stripe account onboarding

newoffer2025
Razmig Sarkissian 5 months ago
parent 381e405d47
commit 4560ebb30a
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 157
      PadelClub/Utils/Network/StripeValidationService.swift
  3. 149
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift

@ -3120,7 +3120,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3166,7 +3166,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3285,7 +3285,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3330,7 +3330,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3374,7 +3374,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3416,7 +3416,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.31;
MARKETING_VERSION = 1.2.32;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -10,12 +10,16 @@ import LeStorage
class StripeValidationService {
static func validateStripeAccountID(_ accountID: String) async throws -> ValidationResponse {
let service = try StoreCenter.main.service()
// MARK: - Validate Stripe Account
static func validateStripeAccount(accountId: String) async throws -> ValidationResponse {
let service = try StoreCenter.main.service()
var urlRequest = try service._baseRequest(servicePath: "validate-stripe-account/", method: .post, requiresToken: true)
let body = ["account_id": accountID]
urlRequest.httpBody = try JSONEncoder().encode(body)
var body: [String: Any] = [:]
body["account_id"] = accountId
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
@ -23,17 +27,79 @@ class StripeValidationService {
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return decodedResponse
case 400:
// Handle bad request
case 400, 403, 404:
// Handle client errors - still decode as ValidationResponse
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return errorResponse
case 403:
// Handle permission error
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
default:
throw ValidationError.invalidResponse
}
} catch let error as ValidationError {
throw error
} catch {
throw ValidationError.networkError(error)
}
}
// MARK: - Create Stripe Connect Account
static func createStripeConnectAccount() async throws -> CreateAccountResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(servicePath: "stripe/create-account/", method: .post, requiresToken: true)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(CreateAccountResponse.self, from: data)
return decodedResponse
case 400, 403, 404:
let errorResponse = try JSONDecoder().decode(CreateAccountResponse.self, from: data)
return errorResponse
default:
throw ValidationError.invalidResponse
}
} catch let error as ValidationError {
throw error
} catch {
throw ValidationError.networkError(error)
}
}
// MARK: - Create Stripe Account Link
static func createStripeAccountLink(_ accountId: String? = nil) async throws -> CreateLinkResponse {
let service = try StoreCenter.main.service()
var urlRequest = try service._baseRequest(servicePath: "stripe/create-account-link/", method: .post, requiresToken: true)
var body: [String: Any] = [:]
if let accountId = accountId {
body["account_id"] = accountId
}
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(CreateLinkResponse.self, from: data)
return decodedResponse
case 400, 403, 404:
let errorResponse = try JSONDecoder().decode(CreateLinkResponse.self, from: data)
return errorResponse
default:
throw ValidationError.invalidResponse
@ -46,17 +112,67 @@ class StripeValidationService {
}
}
// MARK: - Response Models
struct ValidationResponse: Codable {
let valid: Bool
let canProcessPayments: Bool?
let onboardingComplete: Bool?
let needsOnboarding: Bool?
let account: AccountDetails?
let error: String?
enum CodingKeys: String, CodingKey {
case valid
case canProcessPayments = "can_process_payments"
case onboardingComplete = "onboarding_complete"
case needsOnboarding = "needs_onboarding"
case account
case error
}
}
struct AccountDetails: Codable {
let id: String
let chargesEnabled: Bool?
let payoutsEnabled: Bool?
let detailsSubmitted: Bool?
enum CodingKeys: String, CodingKey {
case id
case chargesEnabled = "charges_enabled"
case payoutsEnabled = "payouts_enabled"
case detailsSubmitted = "details_submitted"
}
}
struct CreateAccountResponse: Codable {
let success: Bool
let accountId: String?
let message: String?
let existing: Bool?
let error: String?
enum CodingKeys: String, CodingKey {
case success
case accountId = "account_id"
case message
case existing
case error
}
}
struct CreateLinkResponse: Codable {
let success: Bool
let url: URL?
let accountId: String?
let error: String?
enum CodingKeys: String, CodingKey {
case success
case url
case accountId = "account_id"
case error
}
}
@ -64,5 +180,28 @@ enum ValidationError: Error {
case invalidResponse
case networkError(Error)
case invalidData
case encodingError
case urlNotFound
case accountNotFound
case onlinePaymentNotEnabled
var localizedDescription: String {
switch self {
case .invalidResponse:
return "Réponse du serveur invalide"
case .networkError(let error):
return "Erreur réseau : \(error.localizedDescription)"
case .invalidData:
return "Données reçues invalides"
case .encodingError:
return "Échec de l'encodage des données de la requête"
case .accountNotFound:
return "Le compte n'a pas pu être généré"
case .urlNotFound:
return "Le lien pour utiliser un compte stripe n'a pas pu être généré"
case .onlinePaymentNotEnabled:
return "Le paiement en ligne n'a pas pu être activé pour ce tournoi"
}
}
}

@ -10,6 +10,7 @@ import SwiftUI
import PadelClubData
struct RegistrationSetupView: View {
@Environment(\.openURL) private var openURL
@EnvironmentObject var dataStore: DataStore
@Bindable var tournament: Tournament
@State private var enableOnlineRegistration: Bool
@ -41,7 +42,7 @@ struct RegistrationSetupView: View {
@State private var refundDateLimit: Date
@State private var refundDateLimitEnabled: Bool
@State private var stripeAccountId: String
@State private var stripeAccountIdIsInvalid: Bool = false
@State private var stripeAccountIdIsInvalid: Bool?
@State private var paymentConfig: PaymentConfig?
@State private var timeToConfirmConfig: TimeToConfirmConfig?
@ -50,6 +51,10 @@ struct RegistrationSetupView: View {
@State private var hasChanges: Bool = false
@State private var stripeOnBoardingURL: URL? = nil
@State private var errorMessage: String? = nil
@State private var presentErrorAlert: Bool = false
@Environment(\.dismiss) private var dismiss
init(tournament: Tournament) {
@ -320,7 +325,6 @@ struct RegistrationSetupView: View {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(role: .destructive) {
_save()
dismiss()
}
}
}
@ -331,7 +335,7 @@ struct RegistrationSetupView: View {
HStack {
Button("Effacer") {
stripeAccountId = ""
stripeAccountIdIsInvalid = false
stripeAccountIdIsInvalid = nil
tournament.stripeAccountId = nil
}
.buttonStyle(.borderless)
@ -344,7 +348,13 @@ struct RegistrationSetupView: View {
}
}
}
.alert("Paiement en ligne", isPresented: $presentErrorAlert, actions: {
Button("Fermer") {
self.presentErrorAlert = false
}
}, message: {
Text(ValidationError.onlinePaymentNotEnabled.localizedDescription)
})
.toolbarRole(.editor)
.headerProminence(.increased)
.navigationTitle("Inscription en ligne")
@ -435,32 +445,6 @@ struct RegistrationSetupView: View {
Text("Revenu Padel Club")
}
}
if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
VStack(alignment: .leading) {
LabeledContent {
if isValidating {
ProgressView()
} else if focusedField == nil, stripeAccountIdIsInvalid == false, stripeAccountId.isEmpty == false, isValidating == false {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
}
} label: {
TextField("Identifiant du compte Stripe", text: $stripeAccountId)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._stripeAccountId)
.disabled(isValidating)
.keyboardType(.alphabet)
.textContentType(nil)
.autocorrectionDisabled()
}
if stripeAccountIdIsInvalid {
Text("Identifiant Stripe invalide. Vous ne pouvez pas activer le paiement en ligne.").foregroundStyle(.logoRed)
Button("Ré-essayer") {
_confirmStripeAccountId()
}
}
}
}
} header: {
Text("Paiement en ligne")
} footer: {
@ -488,13 +472,81 @@ struct RegistrationSetupView: View {
.onChange(of: refundDateLimit) {
_hasChanged()
}
.onChange(of: focusedField) { old, new in
if old == ._stripeAccountId {
_confirmStripeAccountId()
}
}
if dataStore.user.registrationPaymentMode.requiresStripe() {
if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
Section {
LabeledContent {
if isValidating {
ProgressView()
} else if focusedField == nil, stripeAccountIdIsInvalid == false, stripeAccountId.isEmpty == false, isValidating == false {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
}
} label: {
TextField("Identifiant du compte Stripe", text: $stripeAccountId)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._stripeAccountId)
.disabled(isValidating)
.keyboardType(.alphabet)
.textContentType(nil)
.autocorrectionDisabled()
}
.onChange(of: focusedField) { old, new in
if old == ._stripeAccountId {
_confirmStripeAccountId()
}
}
if stripeAccountIdIsInvalid == true {
Text("Identifiant Stripe invalide. Vous ne pouvez pas activer le paiement en ligne.").foregroundStyle(.logoRed)
}
if stripeAccountId.isEmpty == false {
Button("Vérifier le compte Stripe") {
_confirmStripeAccountId()
}
.disabled(isValidating)
}
if let errorMessage {
Text(errorMessage).foregroundStyle(.logoRed)
}
RowButtonView("Connecter ou créer un compte Stripe", role: .destructive) {
errorMessage = nil
stripeAccountIdIsInvalid = nil
stripeAccountId = ""
stripeOnBoardingURL = nil
do {
let createStripeAccountResponse = try await StripeValidationService.createStripeConnectAccount()
print("createStripeAccountResponse", createStripeAccountResponse)
guard let accounId = createStripeAccountResponse.accountId else {
throw ValidationError.accountNotFound
}
let createStripeAccountLinkResponse = try await StripeValidationService.createStripeAccountLink(accounId)
print("createStripeAccountLinkResponse", createStripeAccountLinkResponse)
stripeOnBoardingURL = createStripeAccountLinkResponse.url
stripeAccountIdIsInvalid = nil
stripeAccountId = accounId
if let stripeOnBoardingURL {
openURL(stripeOnBoardingURL)
} else {
throw ValidationError.urlNotFound
}
} catch {
self.errorMessage = error.localizedDescription
Logger.error(error)
}
}
} header: {
Text("Compte Stripe")
} footer: {
Text("Vous devez connecter un compte Stripe à Padel Club. En cliquant sur le bouton ci-dessus, vous serez dirigé vers Stripe pour choisir votre compte Stripe à connecter ou pour en créer un.")
}
Section {
let fixedFee = RegistrationPaymentMode.stripeFixedFee // Fixed fee in euros
let percentageFee = RegistrationPaymentMode.stripePercentageFee
@ -526,11 +578,10 @@ struct RegistrationSetupView: View {
// Text("Aucune commission Padel Club ne sera prélevée.").foregroundStyle(.logoRed).bold()
}
}
}
private func _confirmStripeAccountId() {
stripeAccountIdIsInvalid = false
stripeAccountIdIsInvalid = nil
if stripeAccountId.isEmpty {
tournament.stripeAccountId = nil
} else if stripeAccountId.count >= 5, stripeAccountId.starts(with: "acct_") {
@ -544,13 +595,12 @@ struct RegistrationSetupView: View {
Task {
isValidating = true
do {
let response = try await StripeValidationService.validateStripeAccountID(accId)
let response = try await StripeValidationService.validateStripeAccount(accountId: accId)
print("validateStripeAccount", response)
stripeAccountId = accId
stripeAccountIdIsInvalid = response.valid == false
enableOnlinePayment = response.valid
stripeAccountIdIsInvalid = response.canProcessPayments == false
} catch {
stripeAccountIdIsInvalid = true
enableOnlinePayment = false
}
isValidating = false
}
@ -567,6 +617,8 @@ struct RegistrationSetupView: View {
tournament.isTemplate = isTemplate
tournament.isCorporateTournament = isCorporateTournament
tournament.unregisterDeltaInHours = unregisterDeltaInHours
var shouldDismiss = true
if enableOnlineRegistration {
tournament.accountIsRequired = userAccountIsRequired
tournament.licenseIsRequired = licenseIsRequired
@ -589,6 +641,12 @@ struct RegistrationSetupView: View {
tournament.stripeAccountId = stripeAccountId
} else {
tournament.stripeAccountId = nil
if enableOnlinePayment, isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
enableOnlinePayment = false
tournament.enableOnlinePayment = false
shouldDismiss = false
}
}
} else {
tournament.accountIsRequired = true
@ -626,8 +684,11 @@ struct RegistrationSetupView: View {
}
self.dataStore.tournaments.addOrUpdate(instance: tournament)
dismiss()
if shouldDismiss {
dismiss()
} else {
presentErrorAlert = true
}
}
}

Loading…
Cancel
Save